この日記はMozillaのプロダクトへの貢献者としての私の成果を中心に、気になったバグやWeb界隈の話題について書いていますが、 断り書きがある場合を除き、いかなる団体のオフィシャルな見解ではありません。あくまでも個人的なものです。 Mozilla Foundation、Mozilla Corporation、及び関連企業の公式情報ではないことに注意してください。

現在、XHTML 1.0 (もどき)から、HTML5なコンテンツに修正中です。古い日記は修正が完了していませんので表示が崩れます。 順次、修正していく予定ですのでしばらくお待ちください。

もずはっく日記

2018年12月11日

Bug-org 1479964 Tracking event.keyCode issue due to the implementation of window.event 初回投稿日時: 2018年12月11日23時53分08秒
カテゴリ: Events KeyboardEvent Mozilla Core Mozilla65 バグ修正
固定リンク: id=2018121102
リンク元: 0件
SNS: (list)

多くのデキの悪いサイトがGeckoで動かない原因のひとつに、イベントリスナにeventという引数を渡していないのに、event.preventDefault()等と書き、実質的にwindow.eventを参照しているというケースが多々あったそうです。これに対する互換性としてwindow.eventはWHATWGによって標準仕様に取り込まれることになったので、Geckoでもサポートするようにした模様です。

ところが、いざ実際にこれを実装し、有効化してみると、多くのサイトがwindow.eventをIEと同じkeypressイベントが来るかどうかを判定するfeature detectionとしてこれを参照していました。当然、これらに関連性は皆無なので、そのように書かれていると動きません。まさにそのようなアプリのコードはfeature detectionの悪い例です。

そこで急遽、keypressイベントのみ、KeyboardEvent.keyCodeKeyboardEvent.charCodeの値をどちらか非ゼロの値をもう一方にセットしなくてはいけなくなりました。

Geckoでは、keypressイベントのKeyboardEvent.keyCodeの値をまずは先行するkeydownイベントと同じものと仮セットします。次に、KeyboardEvent.charCodeをそのキー入力、もしくは、そのキー入力からCtrlAlt、またはCommandの状態を取り除いた際に入力される文字のUnicodeコードポイントにセットします。最後に、KeyboardEvent.charCode値が非ゼロ場合、KeyboardEvent.keyCodeをゼロにしていました。

しかし、他のブラウザでは最後の段階でKeyboardEvent.keyCodeKeyboardEvent.charCodeの値をセットし、逆に、KeyboardEvent.charCodeの値がゼロの場合(Enterキーの場合)にKeyboardEvent.keyCodeの値をKeyboardEvent.charCodeにセットしていました。

幸いにもnon-printableキーのkeypressイベントの発火を止める修正が進行中だったので、少ないリスクでこの修正を行うことができました。

それにしても、本当に、論理的に考えずにトライアンドエラーだけで実装しちゃう人が多いんですね。一見関連性のあるもの同士を対にしていたために壊れるサイトが多かったことに非常に驚かされました。

Bug-org 968056 keypress event shouldn't be fired for non-printable keys 初回投稿日時: 2018年12月11日23時35分00秒
最終更新日時: 2018年12月12日00時00分39秒
カテゴリ: Events IME KeyboardEvent Mozilla Core Mozilla65 バグ修正
固定リンク: id=2018121101
リンク元: 0件
SNS: (list)

長年、Geckoでは、モディファイアキーを除くキーの入力時に、keypressイベントを発火していました。しかし、UI Events (旧DOM Level 3 Events)ではkeypressは文字の入力時にのみ発火するという定義が(後から)行われました(例えば、aの入力では発火するが、Ctrl+aでは発火しない、また、ArrowDownのように、端から文字を入力しないキーの場合にも発火しない)。このバグ修正は、Geckoの動作をUI Evnetsの仕様通りの動作にあわせたものです。

Geckoではほぼ全てのショートカットキーがkeypressイベントを処理することで実装されています。そこで、まず考えられるのは、全ての内部のkeypressハンドラをkeydownイベントハンドラで書き換えることでした。しかし、ひとつ問題があります。keypressイベントは、keydownpreventDefault()が呼び出されない限りは発火しなくてはいけません。ですので、先行するkeydownイベントでのショートカットキーの実装は無理であると分かりました。

仕方ないので、keypressイベントのリスナを、Gecko内部システムグループに全て書き換えることにしました。Geckoはイベントの発火を毎回2回ずつ行っており、最初の発火でデフォルトグループと呼ばれるリスナ(通常のWebアプリのリスナはすべてこちら)向けに発火し、その後、システムグループのリスナ向けに再発火しています。そう、本来は全てのGeckoもしくはFirefoxのデフォルトアクションは、システムグループのリスナで実装されていなければ、Webアプリのリスナとどちらが先に処理するかというレースが発生してしまうので、これは潜在的だったバグを全て修正していく作業になりました。そしてもちろん、この大きな修正は、大量の自動テストの修正が余儀なくされたり、多くのregressionを出しつつ、潰しつつ、という作業に終始しました。

そして最終的に、上述のデフォルトグループでの発火のみを停止し、Webアプリからはkeypressイベントが、文字の入力がある場合にのみ発火するかのように見えるようになっています。

ちなみに、Enterキーの扱いのみが特殊です。そもそもEnterが押された場合、改行が行われますが、文字が入る訳ではありません。しかし、どのブラウザでもEnterキーが押された場合にはkeypressイベントが発火するので、Geckoもそれにあわせています。しかし、モディファイアキーと組み合わせた場合にkeypressイベントが発火するかどうかはブラウザ依存だったので、GeckoではShift+EnterCtrl+Enterkeypressイベントを発火するようにしています。特にCtrl+EnterはGeckoでは何も起きないのですが。

ちなみに、この修正はGoogleの検索チームから、これを修正しないとFirefox for AndroidのサポートをTier1から外したままにするよっていう宣告が来たのでかなりがんばって修正したんですが、この修正によって一番広範囲に壊れたのはGoogle Closure Libraryと、これをベースにしてるGoogle Docs等、Googleの他のチームのプロダクトだったというのがオチですかね……(Closureのチームと、Google Docsのチームは本当に大変な修正に尽力してくださったことに、あらためてお礼を言っておきます。ありがとうございました。)

Bug-org 354358 keydown/keyup events should be dispatched even during composition (but keypress shouldn't be so) 初回投稿日時: 2018年12月11日23時09分31秒
最終更新日時: 2018年12月11日23時13分41秒
カテゴリ: Events IME KeyboardEvent Mozilla Core Mozilla65 バグ修正
固定リンク: id=2018121100
リンク元: 7件
SNS: (list)

非常に長い長い道のりでしたが、ついにこのバグ修正、そして続けてポストされる記事にある、keypressイベントの挙動を他のブラウザにあわせた修正が完了しました。

UI Events (DOM Level 3 Events)のWGにInvited Expertとして参加していた時に、WGのメンバーと議論を重ね、IMEの未確定文字列がある間(compositionstartからcompositionendの間)、キーボードイベントはどのように発火されるべきか、されないべきかというのを決めて、現在の仕様案のように結論付けられています。

私の主張は、Geckoがkeydownkeyupも未確定文字列がある場合に発火していないこともあり、IMEを知らない開発者に不用意にこれらのイベント中にSelectionを変更されたりして未確定文字列を強制確定しないといけないケース、つまり、IMEユーザがIMEを意識せずに作られたWebアプリで困らないように、全く発火させないという形でした。

一方でWG側の主張は、keydownkeyupはキーボードのキーの物理状態を示すイベントであり、keypressは文字入力を示す、論理状態を示すイベントであるので、keydownkeyupは未確定文字列があっても発火すべき、というものでした。

そして色々とあって、WG側の案が標準仕様として残ることになりました。

このバグの修正により、keydownイベントと、keyupイベントが未確定文字列がある場合にも発火するように修正されています。

これらのkey値は、IMEが処理していた場合にはProcess、そうではない場合はIME無しで入力される文字、もしくは本来のキー名(Enter等)となります。この定義が非常に曖昧なのと、プラットフォーム、もしくはアクティブなIMEによって、どのキーイベントを処理したと主張するかはそれぞれに依存することになりますので、KeyboardEvent.isComposingを参照して、未確定文字列が存在するかどうかを確認する方がシンプルなコードになるでしょう。

ちなみにGeckoでは未確定文字列がある場合、各OS用のキーイベントハンドラが、クロスプラットフォーム部分に対してGecko内のイベントを発火しないようにしていました。このため、最初に全ての内部キーボードイベント、内部IMEイベントをTextEventDispatcherという新しいクラスを介して発火するように書き換え、ここで集中管理することで、長期間、NightlyとEarly-Betaでのみテスタ向けに新しい動作を有効化していました。

2018年12月7日

Bug-org 1497746 Reduce footprint of TextEditor 初回投稿日時: 2018年12月07日00時33分38秒
最終更新日時: 2018年12月12日00時03分38秒
カテゴリ: Editor Mozilla Core Mozilla65 バグ修正
固定リンク: id=2018120701
リンク元: 0件
SNS: (list)

他のバグの修正中に気付いたのですが、<input>要素や<textarea>要素の数だけインスタンス化されるTextEditorクラスが64bit版では544バイトも使っていました。さらに悪いことに、Geckoで採用しているjemallocは、メモリのフラグメンテーションを抑制するために、特定のサイズ内のメモリ確保時には、それが収まる容量用に確保されたエリアに確保されます。このボーダーラインが512バイトということもあり、今まではTextEditorクラスのインスタンスはひとつあたり1024バイトも使うということになってしまっていました。

TextEditorやその基底クラスであるEditorBaseのメンバを確認してみると、そのうちの多くが実際にユーザのアクションに反応して、もしくは、Webアプリからの指示で編集にのみ必要なものが非常に多いことが分かりました。

ここで、Bug-org 1465702の修正が思わぬ形で使えることが分かりました。このバグの修正により、エディタは編集中にのみ、一時的にスタック上にクラスをインスタンス化し、それを簡単に参照できるようにしています。そのため、上述のメンバは全てこの中に移動できるのです。

さらに、メモリフラグメンテーションの抑制と、メモリ確保時のオーバーヘッド削減のために、確実に登録されていたエディタの様々なリスナを登録するための配列に確保していた容量も、Quantum Flowプロジェクトでそのリスナが減った結果、予約しなくてよくなっていることも発見しました。

これらを削減することにより、バグの登録時には512バイトのラインを少し割れば良いかなと思っていたものが、なんと、400バイトにまで削減することに成功しました。つまり、<input>要素や<textarea>要素ひとつひとつで消費されるメモリ量がエディタ部分に関しては半減します。スタック内に一時的に確保すれば十分なものをヒープに確保してはいけませんね、というのを実感したバグでした。

ちなみに、macOSネイティブアプリのアロケータもjemallocらしいので、macOSを使っていると、この辺に明るくないアプリ開発者が作ったアプリに無駄にメモリを使われてる事が多いんではないかなという気はします。

Bug-org 1465702 EditorBase, TextEditor and HTMLEditor should use a stack class to store all information which are necessary to handle each EditAction like TextEditRules and HTMLEditRules 初回投稿日時: 2018年12月07日00時12分26秒
最終更新日時: 2018年12月12日00時03分17秒
カテゴリ: Editor Events Mozilla Core Mozilla65 バグ修正
固定リンク: id=2018120700
リンク元: 0件
SNS: (list)

InputEvent.inputTypebeforeinputイベントの実装時に非常に重要なので、これ(とbeforeinput)を実装するための準備として、Geckoのエディタを大きく書き換えていました。これをブロックしているバグを見ると、なんと5月ぐらいに取りかかってるようなので、実に半年間もやっていたわけです。

Geckoのエディタのコンクリートクラスは、TextEditorHTMLEditorという名前通りのふたつのクラスがあります。前者は当然、<input>要素や、<textarea>要素、後者は、document.designModecontenteditable属性の指定された要素があるドキュメントでそれぞれインスタンス化されています。これらのエディタは、確実にinputイベントを何らかの編集後に発火させるために編集が見込まれるメソッドの各所にスタック上にインスタンス化されるクラスを設置し、それの最後のインスタンスが破棄される際にinputイベントが発火されるようにしています。この設計は普段のメンテナンス性を高める点では優秀ですが、InputEvent.inputTypeの実装を行う今回のように、編集内容をイベント発火時に知ることが困難です(複数の編集がネストすることがあり、それぞれに対して別のinputイベントを発火する必要があるため、なおさらです)。

そこで私が考えたアプローチは、これらの基底クラスも含め、ユーザの入力で編集が始まったとき、もしくは、Webアプリがエディタを通して何らかの編集を開始した時に、スタックに新しいクラスをインスタンス化し、その中にユーザ、もしくはWebアプリが行おうとした編集内容を記録しておき、inputイベントの発火時にはその情報を参照しようというものです。そうすれば、どんなに複雑な編集が行われたとしても、編集内容を示すInputEvent.inputTypeは確実に、なおかつ、的確に設定できることが保証されます。しかし、これを行うには、エディタ外部からの呼び出し時にのみそのインスタンスを生成するという必要があります。

ここでまず問題になったのが、外部から呼び出されるメソッドがそのままpublicメソッドでは無いという点です。なぜなら、publicメソッドはクラス内部からも呼び出されるためです。もし、Webアプリによって、ユーザの編集中に他の編集がネストできないならこれは問題になりません。しかし、Web標準仕様はそれを許しています。つまり、純粋に外部から呼び出される場合にのみpublicメソッドを使用し、エディタ内部で編集中にはprotected以下のスコープのメソッドを呼び出すようにエディタ全体を書き変えるしかありません。

ところがここで問題になったのはこれら全てのクラスのpublicメソッドの多さです。具体的には、これらのクラスはnsIEditornsIPlaintextEditornsIHTMLEditornsIEditorMailSupportnsIEditorStyleSheetsnsIHTMLAbsPosEditornsIHTMLInlineTableEditornsIHTMLObjectResizernsITableEditorといった多くのインターフェースを実装し、さらにQuantum Flowプロジェクトで高速化のために削除した、もしくは複製されたpublicメソッドを多々もつクラスです。

この絶望的な物量を前にしても、内部からも呼び出されるpublicメソッドはprotectedメソッドとして実装し、publicメソッドは外部からの呼び出し時にも用い、対応するprotectedメソッドを呼ぶだけにしていくしかありませんでした。より、C++として安全な実装(ビルド時に内部からpublicメソッドが呼ばれないようにする)があるかもしれませんが、ひとまず、こういった形で半年かけて大きく書き換えたのがこのバグ修正となっています。