2009-05-31
[断線部] 近況
連載がはじまってからプレッシャーに弱い自分をみつけた. 原稿の締切が気になって余暇も落ち着かない. かといってさっさと原稿を書く身軽さもない. 結局なにもせずにぐずぐずと週末を過ごしてしまう. 仕方ないので週末のうち一日は物書きデーと決めてノート PC 持参で街にでかけ, コーヒー屋で原稿かき(か, その準備)をすることにした. "副業出勤" と命名. 私はワイヤレスな IP を持っていないので, 副業出勤は自動的に断線部となる. 今週はまだ二回目. 定着すると良いなあ...
副業出勤をしてみてわかったことが 3 つある.
1. お手洗いに行くのが面倒: 席を外れるときに荷物をもっていく必要がある. いつもならサイフだけ持っていくんだけど, 副業中はノート PC も手放せない. PC かかえてトイレにいくのは相当マヌケ. 同僚が必要だと思った. 世間のスタバ派自営業者はどうしてるんだろう.
2: 野良電源の確保には厚顔さが必要: コーヒー屋の机の下に電源を発見したものの間借りしたら怒られそうだと躊躇していたら, 隣席にやってきた人が躊躇なく挿していた. 自分には厚かましさが足りない. 会社の同僚は許可を得てから使うことが多いという. チキンな私にはそれも敷居が高い.
3: バッテリーより集中力が希少: 今までのところバッテリーを使い切るほど作業を続けられたことがない. 電源の心配は杞憂かもね...
コーヒーをすすりながら, 次回に向けて WebKit のコードを読んだ. とりあえず CSS の話でもするかとコードを眺めて下書きをはじめたものの, ふと CSS の前に DOM の話をする必要があるなと考えを改め, けれど DOM なんて単なるツリー構造だから一回分には分量不足だなあ... などと頭を悩ませ, 糖分を求めてドーナツをつまむ.
そして分量稼ぎに JavaScript と DOM の binding を紹介するのはどうかとブリッジ部分のコードを読み, でもインタプリタのランタイムは面倒な割に面白くないし, そもそも DOM の話だけで一回使ってしまったら全 6 話完結はムリそうだと再び頭を悩ませ, ドーナツ追加の誘惑をぐっと堪え, そういえば WebKit は DOM まわりのメモリリークをどう対処してるんだろうと気になり調べてみた. が, 読んだかんじリークするようにしか見えない. もうやだ... バッテリーは 70% くらい残ってるけれど気力が尽き, 杞憂に胸を撫でおろし肩を落として帰宅した.
WebKit での言語間巡回参照対策(ではない解決)
とはいえこの悪名高い問題が放置されているとも思えない. 帰宅後にあんぱんをつまみつつぐぐったところ, 今年の 4 月に r42256 で修正されていた. (手元にあるワーキングコピーはちょっと古かった.) 原稿の進まない現実逃避の番外編として紹介してみたい.
まず問題を復習しよう. DOM のメモリリークには色々なパターンがあるらしいけれど, 今回修正されたのはクロージャをイベントリスナに使うと起こるリークだった.
##GooglePrettify
var el = document.createElement("div");
var fn = function() { alert(el.innerHTML); };
el.addEventListener("whatever", fn, false);
// このあと el をツリーに append したり remove したりする.
このコードは以下のような巡回参照をつくる.

参照が JS と C++ をまたいでおり, 問題をややこしくしている.
JS 上の DOM オブジェクト el (JSNode クラス) は, C++ 上の DOM オブジェクエト node (Node クラス) への参照 (1) をもっている. (連載で教わったこと: 矢印には番号をつけよう!) el から node に向かうこの参照は, 参照カウント用のスマートポインタである. イベントリスナは DOM ノードとは逆向きの参照 (2) をもつ.
C++ 上のイベントリスナ listener オブジェクト(JSProtectedEventListener クラス) は, JS 上の関数オブジェクト fn (JSFunction クラス...だとおもう) への参照を持っている. C++ から JS のオブジェクトを保持するときは, JS のオブジェクトを特別な protect 集合に追加する. protect 集合に含まれたオブジェクトは GC の際にいつもマークされる. (そういえば V8 には Handle という似たような仕組みがあった.)
C++ 上の node はイベントリスナへの参照を C++ のオブジェクトである listener を介して持つ (3). (3) は (1) とおなじく参照カウントの参照.
そして最後に JS の関数 fn は, クロージャ経由で el を参照している (4).
本来なら JS のオブジェクトはマークスイープの GC で回収されるはずだが, fn は protect されているため回収され損なうう. とばっちりで el も回収されない. 更に C++ のオブジェクトである node は参照カウンタが減らず解放されない. listener も同様. めでたくリークがおこる.
JSObject::mark() のフックをつかった JS 側 GC とのインテグレーション
r42256 の修正は, 概念的には (2) の参照を特別な protect 参照から普通の JS の参照に変更する. (赤丸部分)

ただし listener は JS のオブジェクトではないから, GC のマークフェーズは (2) の参照に到達できない. したがって fn はマークされようがない. そこで r42256 では, el がマークされる際のフック処理 の中で el -> node -> listener と参照をたどり, ガーベジコレクタにかわって fn をマークしている.
##GooglePrettify
// JSNodeCustom.cpp
...
void JSNode::mark() // mark() フェーズで呼ばれるフック
{
...
Node* node = m_impl.get(); // C++ の node オブジェクトをとりだし...
...
DOMObject::mark(); // JS オブジェクトとしてのマーク処理をしたあと...
markEventListeners(node->eventListeners()); // node のもつイベントリスナの配列をマークする
...
}
...
##GooglePrettify
// RegisteredEventListener.h
...
inline void markEventListeners(const RegisteredEventListenerVector& listeners)
{
for (size_t i = 0; i < listeners.size(); ++i)
// markJSFunction() は JS 由来のイベントリスナだけに実装がある仮想関数
listeners[i]->listener()->markJSFunction();
}
...
##GooglePrettify
// JSEventListener.cpp
void JSEventListener::markJSFunction()
{
// いざマーク!
if (m_jsFunction && !m_jsFunction->marked())
m_jsFunction->mark(); // ←この m_jsFunction が fn オブジェクト
...
}
el がマークされると, イベントリスナとして登録された fn もマークされることがわかる. (JSProtectedEventListener は JSEventListener に置きかえられている.) 仮想関数として JSObject::mark() をフックできる JavaScriptCore の特徴をうまくつかい, わずか 300 行に満たない変更で大きな問題を片付けることができた. 見事なハックだと思う.
ハックと呼ぶにふさわしい行儀の悪さもある. 仮に JS 上で el への参照がなくなると何がおこるだろう? 参照のなくなった el が回収されると, fn もマークされず同時に回収されるはずだ. けれど listener 内の m_jsFunction 変数には死んだ fn への参照が残っている. dangling pointer で クラッシュすることはないのだろうか.
node cache
この懸念は "node cache" という別の仕組みで回避されている. node cache は, DOM のノード (上の図だと node オブジェクト) に対応する JS のオブジェクト (図の el) を Document オブジェクトに付属した(C++ 内の)連想配列に保存する.

document.getElementById() のように C++ の DOM から JS のオブジェクトを取り出す操作では, まずこのキャッシュを検索してヒットすればその結果を返す. node cache のおかげで複数の独立した document.getElementById("foo") 呼び出しが 同一の JS のオブジェクトを返すことができる. node cache なしで素朴につくると, getElementById() はノードの対応関係を無視して 毎回新しい JS オブジェクトを返してしまうだろう. それを防ぐのが node cache の役割である.
node cache は保持している JS オブジェクトの寿命を(これもフックの中で)独自に管理する.
##GooglePrettify
// JSDOMBinding.cpp
void markDOMNodesForDocument(Document* doc) // JSDocument::mark() から呼ばれる
{
JSWrapperCache& nodeDict = doc->wrapperCache();
JSWrapperCache::iterator nodeEnd = nodeDict.end();
for (JSWrapperCache::iterator nodeIt = nodeDict.begin(); nodeIt != nodeEnd; ++nodeIt) {
JSNode* jsNode = nodeIt->second;
if (!jsNode->marked() && isObservableThroughDOM(jsNode))
jsNode->mark();
}
}
このマーキングによって, キャッシュ内の JS オブジェクトは DOM ノードが observable である限り, JS 上から参照されなくなったあとも生き続ける. DOM ノードは Document ツリーに所属している限り observable だが, Node.remeoveChild() などで削除されると observable でなくなる. observable でなくなった node に紐づく JS オブジェクトはマークされず, GC に回収される. node cache をある種の弱参照だと捉えることもできるだろう.
キャッシュの中身は JS オブジェクトのデストラクタからクリアされる. observable でなくなる -> マークされない -> 回収される -> デストラクタ -> キャッシュクリア という塩梅.
##GooglePrettify
// JSNode.cpp (自動生成されるコード)
JSNode::~JSNode()
{
// この中でキャッシュをクリアする
forgetDOMNode(m_impl->document(), m_impl.get());
}
話が脇道にそれたけれど, この node cache のおかげで dangling pointer はおきない. listner を保持する node が Document から削除されるまで, listener 経由で fn をマークする el は node cache の中に生き続けるからだ.
綱渡りと文脈主義
このように, r42256 はいくらか危うい橋を渡っている. DOM ノードや対応する JS オブジェクトは observablity という DOM ツリー固有のアイデアに依存している. 今回の変更はこのややこしい仕組みにリスナを追加し, ややこしさに輪をかけた.
ややこしい上に制限もある. observability が使えないケース, たとえば Worker や XHR オブジェクトのイベントリスナはどうやって寿命を扱えば良いのだろうか. 詳しく調べてはいないけれど, そのままの仕組みは使えそうにない. そのほか node 以外が listener を参照していないという前提も, 危ういとまでは言わないまでも自明ではない. WebKit のコードに手を入れる人はこうした点に気を配る必要がある. 骨の折れる仕事だろう.
トリックが JavaScriptCore 固有なところにも不安がある. V8 はどうすりゃいいの? 知らねーよ, という主張が正しい気もするけど, 最近 Chrome を使っている身にはやや悲しい.
一方で問題を "言語間の巡回参照解決" のような大問題として捉えるのではなく "DOM ノードをクロージャにもつイベントリスナのリーク" と捉え, 文脈を活かして小さく片付ける様は小気味良い. 規律と混沌, consistency と context の票読みは続く.
来週はちゃんとが原稿が書けますように...
関連リンク
- Bug 241518 - calling addEventListener with a closure holding a content node leaks the document: Mozilla の同様のバグ(と修正. たぶん.)
- A Cycle Collector on Gecko : C++ 内の巡回参照とその解決
2009-05-09
近況

InfoQ にはけっこう気になるビデオがあって, たとえば Erich Gamma が Eclipse について語っていたりする. でもビデオをみるのはそれなりに気合を消費するから, スライドだけみてわかった気になりたいなーと思ってページのスライドビューアに ページめくりボタンを探すも見当たらない. ので user.js でつくってみました. 動画に弱い InfoQ ファンなら少しは役に立つかもしれません. infoq_slide_navigator.user.js (github のページ) ページ数のテキストフィールドにフォーカスした状態で "p" や "n" のショートカットが有効です. (j/k も可.) ページめくりすら面倒な気分に備え, 全スライドまとめて表示もあり. Firefox3.5b4 for MacOS で動作を確認. 他でも動く気がする.
ついでにスライドだけでもわかった気になれる(=話は聞いてない)スライドを, 今年に入ってから公開された講演からいくつか紹介してみる. 講演自体は 2008 年の QCon などが中心のようす:
Eclipse の歴史, のような話.
Java は普及しちゃってるから仕様かえるの大変なんだという話と, 新しく提案されている仕様(string switch, operator overloading, ...)について.
HTTP の仕様を(主にバグ取り目的で)改訂する HTTPbis という WG の活動の紹介. あとは PATCH メソッド なんてのが提案されてるぜなど最近の HTTP の動向. IETF 的に Oxxx 一味はどう思ってるの?とか色々面白い.
基本的には "プロダクティブプログラマ" 著者による宣伝. ThoughtWorks ファン向け. "corporate code smell" というのが面白かった. smell 6, "弊社では WebSphere を採用しておりますなぜなら..." ...名指しかよ.
前にちょっと紹介した 気もする Steve Vinoski が REST のすばらしさを語る. そうですね素晴しいですね, というかんじ.
動的言語を JVM に載せるにはどんな機能が足らないのかという話, それを補う Da Vinchi Machine Project の紹介. 動的言語が JVM に要求するもののリストはなるほどなーとおもった. ダビンチはよくわからず.
Yahoo は YQL というのを提供しており, こんなかんじのクエリー言語ですよ, という紹介. へーそんなかんじなのね. という感想. あとは Yahoo! Pipes を運営してみた感想など.
...
以下はスライドみても話の中身はわからなかったけれど, 話を聞いてみたくなったスライド.
ピラミッド発掘の写真が延々でてくる. 講演者もエジプト専門家. でも講演をしたのは OOSPLA2008. なんだそりゃ.
CouchDB 開発者が語る "Other people work on cool stuff.... WHY NOT ME?" など. スライドの放つアホオーラに引力.
Sun 社員が語るプログラミングと Jazz の関係. なんだそりゃ.
これはスライドはどうでもよくて, 私が Michael Feathers のファンだから...
...
このほかにも Facebook やら Salesforce やら GData API やらの流行っぽい話も多く, 一時間聞くのはやだけどスライドを眺めるくらいならいいかな, という程度に気になるものはけっこうあった. 我ながら便利で満足.
2009-05-03
明日の空模様
ある Mac プログラマから "WebKit ってドキュメントがすくないよね" という話を聞いた. WebKit の挙動が気に入らないので直せないかと調べたところ, Mozilla と比較した文書の少なさに呆れたという. 私としては連載枠が貰えたのもひとえに文書がないおかげだから, そこに不満はない. (むしろめでたい.) けれどコードを直したい人が不便なのは困ったことだ.
WebKit に比べ, Mozilla の内部は網羅的ではないにしろそれなりに文書化されている. この違いはどこから来るのだろう.
私の印象は, Mozilla が "書き直し" プロジェクトだからというものだった. Netscape は Netscape Natigator 4.x のコードベースを捨て, ほぼスクラッチから Gecko を書いたとされている. 私の仄暗い経験によれば, 書き直しのプロジェクトは最初にちゃんと "設計" をしようとする傾向がある. 素朴なソフトウェア観に従うと, あるコードベースが保守できないほど酷くなるのは, "最初にちゃんとした設計をしなかったから" だという結論されることが多い. だから次は "ちゃんと設計しよう" と考え, ドキュメントを書いたりする.
けれど, そもそもプロジェクトの開始時点ではまだ "対象を理解していなかった" と見ることもできる. ソフトウェアの扱う対象, 解こうとする問題を開発者が理解していなければ, 設計は決まりようがない. 下手に決断しても飛ばないロケットができるだけだ. 未知の問題に挑むなら多少の困難が伴うのは仕方がないこと. 理解の程度それ自体の自覚に大きなズレがないかぎりは悲観することでもないだろう.
この見方に従うと, 無知から生まれた間違いを後で正せなかった <ソフトウェアの成長不良> こそが 未知の問題に挑んだコードが保守できなくなる原因だと捉えることができる. 書き直しがうまくいくのは big design upfront の勝利ではなく経験の勝利なのだ.
だから経験を明文化する意味で文書を書くのには意味がある. けれど希望に溢れる書き直しプロジェクトの文書にそうした過去の影を見ることは少ない. あおく柔らかな芝生の眩しさばかりが目を刺す. 次世代版はポータブルなメニーコア VM 上に構築されたスケーラブルでディストリビューテッドかつモジュラーでプラッッガブルなアプケーションプラットホームですかそうですか.
口あたり
話が逸れた.
件の Mac プログラマは, 私の <二度目だから文書が多い> 説に異議を唱えた. かわりに Gecko に比べ WebKit のコードは <すっきりしている> から, ドキュメントがなくても良いんじゃないかという. WebKit コードベースのすっきり加減についてはいくらか異論があるものの, 彼の指摘は的を射ている. つまりコードのスタイルと文書化の程度には相関があるかもしれない.
柔軟性や再利用性を高めようと間接化, 仮想化, パラメタ化をすると, コードは本来の対象と少し <距離> ができる. 問題と距離のあるコードは読み手にとってわかりにくい. それは修辞や隠喩に覆われた文章と似ている. その距離を埋める手っ取り早い方法として文書が選ばれる. 設計の背後にある意図を自然言語で説明すれば, ニブチンな読み手もわかってくれるだろうというわけ.
この <距離> を単に <抽象化> と読ぶのはおそらく乱暴すぎる. 抽象が必ずしも距離をうむわけではない. 距離を縮める, 理解の助けになる抽象もある. 持ち込まれる距離は抽象によって異なる. 先の 3 つ のように柔軟性をもたらす抽象は距離をつくりやすいとおもう. ぴったりくっついていたら相手の変化に振り回されてしまうからね.
改めて眺めると, 文書が多いとされる Gecko は XPCOM ベースの仮想化過剰なコンポーネント群で, 文書のない WebKit は具象クラスと相互依存のモノリシックだ. どちらが良いかはともかく, コードから対象までの距離が短く見通しが良いという意味で WebKit のコードを <すっきりしている> と評するのはありかもしれない. 便乗して Gecko のコードはコクがある, なんてのは口から出まかせにも程があります. 識者のテイスティング求む.
目くばせ
話が逸れないうちに本題へ進もう.
不要な抽象を持ちこまないのは YAGNI の大きなテーマだ. YAGNI に従うなら, 本当に必要になるまで, つまり実際に再利用や変更がおこるまでは距離をつくる抽象を持ち込むべきでない. 一方で, 経験からいずれそうした抽象が必要になるとわかることもある. あとで変更するのが目に見えているだけに二度手間が推しい. 今のうちに抽象をつくりこみたい. 絶対必要だってば. 私のなかの博打打ちがそう告げる. でも待って. <いずれ> や <今のうちに> は典型的な YAGNI 違反の堕落ですよ. 私のなかの清教徒がすかさず咎める. 睨み合いが始まる.
結果主義と投機気質の板ばさみにあった玉虫色の私は, 結果主義者の顔をたてながら投機屋への隙を残すようになった. 実際に距離をつくることはしないが, いつでも距離をおける道を選ぶ. そして投機屋がそれに気付くことを祈る. たとえば, インターフェイスの抽出はしなしなくても具象クラスを特定する箇所(インスタンス化など)は減らしておく. 新しいクラスの抽出はしなくても関係の深いプロパティの宣言をまとめておく. メタプログラミングはしなくてもそれとなく名前づけのルールを揃えておく. データドリブンにはしなくても値の設定を局所化しておく... 投機屋なら嗅ぎ分けられる, 結果主義者には気付かれないこうしたサインを, 私は <目くばせ> と呼んでいる. 目くばせのうち運のよかったものは, やがて本物の抽象に引き上げられるだろう.
玉虫色の腰抜けながら, 目くばせには良いところもある. 目くばせは投機的抽象ほどコストが高くない. 間接化によるリーダビリティや凝集の低下, メタプログラミングに伴うデバッグのしずらさ, データ駆動にありがちなデプロイのトラブル. 目くばせはこうした面倒を招かない. また, 目くばせは博打が外れても傷が少ない. 一度持ちこんだ距離を取り除く作業は(リファクタリングブラウザの支援があってもなお)面倒になりがちだ. けれど目くばせの段階ではまだコード上に距離はない. 間違いに気付いたらひっそり取り下げればいい.
結果主義者に気付かれたために....というよりは気付かれなかったために, 目くばせを駄目にされてしまうこともある. たとえば秘めやかな規約を踏みつけて場違いな名前をつけられてしまう. 内心では宣言的なつもりだったデータの設定に分岐やループを持ち込まれてしまう. 投機屋にとっては悲しいことけれど, これは目くばせが相手をアフォードしなかった, つまり適切ではなかったと考える方が自然だろう.
目くばせと投機的抽象の違いは, コードの将来に対する負担にある. 目くばせ自体には多少の手間がかかるかもしれないけれど, そのせいで複雑さが増すことはない. 平たく言えば後腐れない. 投機的抽象は目にみえる複雑さを持ちこむ. (複雑さというのに抵抗があるなら洗練と言ってもいい.) 見通しが外れた複雑さ/洗練はゴミとして残り, コード環境を汚染しつづける.
目くばせと YAGNI に違いはあるのだろうか. 本来は同じものだと思う. ただ, YAGNI の教条主義的な側面がプログラマの経験や予感から目を逸らさせる嫌いはある. また原理主義的なアジテーションに反発する気持ちが本来の意図に水を差すこともあるだろう. そう考えると, <目くばせ> は伝言ゲームによって失なわれつつあった YAGNI の肌ざわりを 別の言葉で説明しただけなのかもしれない.
快晴
そんな話をリファクタリング原理主義者の友達に話したところ, 別の見解を教わった. (リファクタリング原理主義者とは, ソフトウェア開発を 試行錯誤による理解の螺旋 だと強く信じる一派をいう. Eclipse, VisualStudio ユーザに多い.)
彼らの立場によれば, <距離> を気にする時点で何かが間違っている. よくリファクタリングされたソフトウェアは問題に対する書き手の理解を完全に反映しているはずで, 理解を完全に反映する以上そこに <距離> はないはずだ. 柔軟性や再利用性も, それが本当に必要なら問題に含まれる. 距離の概念がはらむ投機性はない.
別の言い方をすれば, <距離> は問題の不十分な理解(または理解の不十分なコード化)を示唆している. そして <距離> の存在を許している点で投機的抽象も目くばせも大差がない. 投機的抽象は怪しいモデルを押しつけることで, 目くばせはあるべき抽象を見逃すことで, 理解とコードの関係を曇らせてしまう. だからコード上の試行錯誤によって ある瞬間/スライスの適切な抽象を, 時には抽象がいらないという理解を確かなものにして, 曖昧さの雲を晴らすべきだ.
彼はこのように主張し, 続けて訴えた: 確かな理解を得てコードの快晴を保つために必要なものは, 高い試行錯誤(=テストとリファクタリング!)の技術と, 試行錯誤こそが良いソフトウェアをつくるという信念ではないか...
曇りない快晴のコードは YAGNI に則っている. 何かが必要 <かもしれない> という曖昧さがないからだ. この YAGNI は, 私が目くばせとして正当化を試みたものより高い水準を満たすように見える. これを <目くばせの YAGNI> に対し <快晴の YAGNI> と呼ぼう. リファクタリング原理主義者が水準の高い <快晴の YAGNI> を標榜するのは自然なことだ. YAGNI は リファクタリングの論拠であり, リファクタリングは YAGNI の技術的基盤である. 絆は深い.
曇り空の下で
YAGNI には二つの顔がある.
まず不確実性という現実に折合いをつける経験則として <目くばせの YAGNI> がある. ここでは "明日できること" を "不確実で曖昧なもの" と捉える. 不確実さを強調した上で, それとどう付き合うべきかの指針を示している.
次に理想のソフトウェアをあらわす <快晴の YAGNI> がある. ここでは "明日できること" を "今日やること" の補題とみなす. そして "今日やること", つまり今ある問題への完全な理解を通じて "明日できること" の金メッキを剥ごうとする.
快晴の理想に照らされると, 目くばせが所与としたはずの曖昧さは姿を消す. けれど現実のソフトウェア開発には湧き上がる曖昧さの雲とそれを薙ぎ払う試行錯誤の戦いがあり, 空模様は二つの間の関係で決まる. おそらく多くのソフトウェアは曇天に押しやられつつ, どうにか嵐を逃れてリリースに漕ぎ着けるのだろう. 空を覆う雲は 負債 と呼ばれることもある.
リファクタリング原理主義者は, 快晴は正義であり, 商業的な成功にもつながると信じている. 信念を貫くために試行錯誤の技能を高め続けている. 私はそこまで強い信念を持つことができない. 信念を保つには試行錯誤の腕が足りない. いつも嵐の予感に怯えている. 遠い雷鳴に身を震わせ, 青空に焦がれながら雲の切れ間を追いかけて歩みを早める.


bgnori [「もくもく会」とかはいかがでしょうか? http://moku2.g.hatena.ne.jp/]
omo [よさそうですね。なぜみな平日なんだろ。えらい。 ]