WebKit(JavaScriptCore)に100個のPull Requestがマージされた
今年の 2 月から WebKit の JavaScript 処理系である JavaScriptCore に Pull Request を投げ続けています。
JavaScriptCore のソースコードは WebKit のリポジトリ https://github.com/webkit/webkit に完全に含まれています。なので、僕が Pull Request を投げる先も WebKit のリポジトリということになります。そして先日、WebKit リポジトリにマージされた自分の Pull Request の数が 100 に達しました。
Pull Request を作るという活動をやめなければ当然いつかは 100 個に達するので別に偉業ということはないんですが、どちらかというと大して意味のないことをやめられなかったというネガティブな気持ちの方が強くあります。[1]
とはいえ、JavaScriptCore に対して1年足らずで 100 個の Pull Request を作成してマージされる、というのはまあまあ珍しいことのようです。実際に、2024 年内にマージされた JavaScriptCore に対する PR の作者を円グラフで表すと、以下のようになっています。
これを見ると、自分は Constellation さんに次いで 2 番目のようです[2]。そして、この円グラフに現れているほとんどの人はAppleかIgaliaかSonyの従業員なので、趣味でここまでやる人は本当にあんまりいないようです。
こういうことをしている人は良くも悪くも珍しいだろうと思うし、再現性がなくはないと思うので、モチベーションや学習、具体的な Pull Request の内容について記録しておきます。
モチベーション
私が JavaScriptCore への貢献を始めた一番の理由は、好きな言語のことをもっとよく知りたいからです。私が JavaScript を好きな理由は今度どこかで書くとして、JavaScript をもっとよく知るための方法として、その処理系に継続して貢献するというのが効果的なのではないかと考えました。
なぜV8やSpiderMonkeyではなくJavaScriptCoreなのかと聞かれることがよくありますが、GitHubで開発されている唯一のメジャーJS処理系だったからという安直な理由で選びました。
また、言語処理系の実装についてもっと知りたかったというモチベーションもあります。過去に CRuby のソースコードを読んだときに、面白かったのですが、自分が Ruby のことを知らなさすぎるせいでそこまでのめり込むことができませんでした。JavaScript であれば仕様レベルである程度把握しているので面白く読めるのではないかと思ったのです(結果としてこれはあたっていました)。
勉強したこと
WebKit(JavaScriptCore)のソースコードを読んだり書いたりするにあたって、新たに勉強したことを紹介します。
C++
JavaScriptCore のほとんどの部分は C++で書かれているため、JavaScriptCore に対して Pull Request を作成するためには C++を読み書きできる必要があります。
自分は C++を書いたことがほとんどなかったので C++の勉強から始めました(C 言語はなんとなくだが書ける)。どこから勉強して良いかわからなかったので、とりあえず以下の書籍を読みました:
これは結果として良かったと思います。C 言語と比べたときの C++の考え方がある程度わかった気がします。特にEffective Modern C++。自分の感覚として難しかったのはやはりムーブセマンティクスあたりかなと思います。C++にありがちな、一見不自然に見える挙動はそういうものだと覚えてしまえばいいのですが、ムーブセマンティクスはちゃんと理解の努力をする必要があったかなと思います。
C++を使って自分の手でプロジェクトを設計したわけではないし、JavaScriptCore 内で大規模なコードの変更を行ったわけでもないので、今でも C++のことはあんまりわかりません。ググりながら試行錯誤しているのが現状です。cpprefjp には大変お世話になっています。
そしてC++のことが好きになってきました。
コンパイラ一般
自分はコンパイラがどのように動いているのかなんとなくは知っていましたが、詳細は知りませんでした。特にコンパイラの最適化についてはほとんど知りませんでした。そこで、中田育男先生の「コンパイラの構成と最適化」を読みました。高額な書籍ですが、大学にあったので無料で読むことができました。ありがとう筑波大学。
それはそれとして、この書籍は割と分厚く内容も重厚です。今回は構文解析については関心がなかったのでそのへんはスキップして後半だけ読みました。それでもまあまあヘビーでしたが、なんとか読みました。
これによって、制御フローグラフや静的単一代入形式の概要や、それを使った最適化手法などの概要を把握できました。これらの知識は、後述する JSC に関するブログ記事を読むときにも役にたちました。
ガベージコレクションに関しては「ガベージコレクション 自動的メモリ管理を構成する理論と実装」を過去に読んだことがあったので、今回特に新しく勉強することはありませんでした。
JSC
大規模なソースコードを読むときは、詳細なコードを読む前にもっと抽象的なアーキテクチャを把握するべきです。そこで、まず JSC のアーキテクチャについて勉強しました。幸いなことに Apple の JSC チームのメンバーが書いてくれたブログがいくつかあり、それらを読むことでアーキテクチャの外観を掴むことができます。
自分が読んだブログは以下です:
- Filip Pizlo. 2022. Speculation in JavaScriptCore
- Filip Pizlo. 2017. Introducing Riptide: WebKit's Retreating Wavefront Concurrent Garbage Collector
- Haoran Xu. 2022. Understanding Garbage Collection in JavaScriptCore From Scratch
- Justin Michaud. 2020. A Tour of Inline Caching with Delete
- Filip Pizlo. 2016. Introducing the B3 JIT Compiler
これらのブログ記事の中にはとても長いものもあり、読むのに時間がかかります。
これらのブログ記事は JavaScriptCore に限らず、コンパイラや GC の勉強として興味深いものばかりです。特に、Riptide のライトバリアは面白すぎて、テンションがめちゃくちゃ上がってしまい、研究室の同期に面白さを力説してしまいました。単純に趣味としてオススメです。
コードの読み方
これは勉強したことではないですが、「どうやってでっかいコードベースを読むんですか?」という質問をもらったことが何度かあるのでコードの読み方についてもここで紹介します。
前述のとおり全体的なアーキテクチャや用語を把握するのは前提として、その上で自分は基本的にはめちゃくちゃprint debugしてます。あとJavaScriptCoreではlldb、gdbなどのデバッガが割と動くのでめっちゃブレークポイント貼って変数の中身とかみてます。
あと、オプションをつけるとバイトコード列や、JITのディスアセンブル結果が見られるのでそれを睨むこともあります。
再現性のあるアプローチとしては、やはりそのプロジェクトでのprint debugのやり方を最初に見つけることが重要だと思います。特に、JSCではJITコンパイル後のコードからprintするための特別な関数があったりする[3]ので、そういうのを先に見つけておいたのは良かったと思います。FTL JITの中間表現レイヤでprintする方法がどうしてもわからなかったときはWebKit Slackで聞いたらAppleの人が教えてくれました[4]。
https://blog.jxck.io/entries/2024-03-26/chromium-contribution.html みたいな感じでどこかにまとめておくべきかとも思っています。
やや情報が古いこともありますが、JavaScriptCore CSI: A Crash Site Investigation Story にいくつかprint debuggingのやり方が載っています。
具体的な貢献
JavaScriptCore のようにフルタイムのコミッターがいてちゃんとメンテナンスされている言語処理系に対して、外部の人間が貢献できるような箇所を見つけるのは難しいと思う人もいるかもしれません。が、実際はそんなことはなく、頑張って探せばたくさん見つかります。
それぞれの種類ごとに、実際のコミットへのリンクをいくつか載せています。
バグ修正
JavaScriptは仕様と実装が明確に分かれているので、仕様と実際の動作に違いがあったらそれはバグです。test262でコケているのを見つけたり、仕様や実装を睨むことで見つけることができます。Bugzillaに記載されているものもありますが、簡単なものは大体修正されているので自分で見つける方が早いかもしれません。
修正が簡単なものは直しましたが、修正することによって大幅に性能が悪くなったり、そもそも修正するのが難しかったりして諦めたものも結構あります。
- [JSC] RegExp quantifier should allow 2^53 - 1
- [JSC] Object.assign shouldn't do batching when sources argument contains target object
test262やUCDの更新
test262[5]やUCD(Unicode Character Database)の更新が滞っているようだったので適宜手動で更新しています。
リファクタリング
継続的にコミットしているとリファクタリングするべき箇所もわかってくるので、明らかにリファクタリングするべきときはしています。
パフォーマンス改善
JITの新しい最適化フェーズの導入ややGCアルゴリズムの改善のような多くのケースに適用できる最適化を行うのは難しいですが、特定のビルトイン関数の実行を速くすることは割と簡単にできます。最近はArrayのメソッドを高速化することに熱中しています。ナイーブなforループで実装されている配列のコピーをmemcpy
とかmemset
にしたり、C++の標準ライブラリ(std::reverse
とか std::fill
とか)とか使うだけで割と速くなります。
配列の内部表現を理解する必要はあるけど、一回わかってしまえば色々な関数に対して似た方法が適用できるかなという印象。とはいえ、自分の手でWebKitで動くmemset
とか書くのはちょっと緊張しますね。
DFG、FTL JITをいじるような最適化も一個進めているのですが、なかなか難しくてまだマージできていません。
- [JSC] Add fast path for array.concat()
- [JSC] Implement Array.prototype.fill in C++
- [JSC] Implement Array.prototype.toReversed in C++
Normative Changeの実装
Normative Changeというのはざっくりいえば、仕様のリファクタリングを除くJavaScriptの仕様への変更のことです。新しい提案の追加もNormative Changeに含まれます。新しいものとか地味なものは実装されていないことがよくありますので、簡単にできそうなやつは結構やっています。デカいのだとIterator Helpersを半分くらい実装しました。あとは Intl.DurationFormat
とか Intl.Locale
の未実装だったNormative Changeを何個か実装しました。
- [JSC] Implement some/every/find from Iterator Helpers Proposal
- [JSC] Limits values for Intl.DurationFormat and Temporal.Duration
今後
あんまり手が回っていなさそうなNormative Changeへの対応やビルトイン関数のパフォーマンス改善を中心に継続していこうと思っています。
ただ、JSCへの貢献をやめる気はないですけど、もう少し現実と向き合う時間を増やすべきだと思っています。