プログラミング
カスタムビジュアライゼーションレンダラーをSVGからCanvasに書き換えました
We rewrote our custom visualisation renderers from SVG to be in Canvas (polarsignals.com)
要約
Polarsignalsは、プロファイリングダッシュボードの刷新において、SVGベースのレンダラーをCanvasベースのものに書き換えた経験を共有しています。これにより、数万のDOMノードを管理するオーバーヘッドがなくなり、ピクセル描画に移行したことで、ズーム、リセット、スクロールといった操作が瞬時に感じられるほど高速化しました。データは列指向のArrow形式で提供され、ゼロコピーで効率的にCanvasに描画されます。
全文翻訳
私たちはプロファイリングダッシュボードの刷新に懸命に取り組んでおり、その過程で観察された学習と改善を、数値で裏付けながら共有したいと思います。これは、フレイムグラフを高速化することについて書いたのが初めてではありません。昨年、SVGレンダリングを並列化し、大規模なクエリの初期レンダリング時間を約8秒から約91ミリ秒に短縮しました。しかし、SVGには限界があり、その限界はDOMノードで構成されています。フレイムグラフの各フレームは<rect>と<text>です。したがって、大規模なプロファイルは数万のフレーム、つまり数万のDOMノードを意味し、各ノードについてブラウザはスタイル設定、レイアウト、ペイントを行い、後でガベージコレクションできるツリーに保持する必要があります。タブがフリーズするのを防ぐために、古いレンダラーは1フレームあたり約500個のノードをバッチ処理して描画していました。これは機能しましたが、DOMのバッチ処理は症状を治療しているにすぎません。ここでの根本的な問題は、ブラウザに膨大な量の構造を管理させていたことでした。正直に言うと、その投稿の最後にほとんど言及しましたが、フレイムグラフにDOMを使用するのをやめることが次の本当の進歩になると疑っていましたが、Canvasが実際にどれだけ効果があるかは不確かでした。この投稿は、私たち自身の未解決の質問に答えるものです。tl;dr新しいプロファイラーダッシュボードが速くなったのはなぜですか?古いレンダラーはSVGで描画されていました。数万のフレームごとに1つのDOM要素と小さなJavaScriptオブジェクトがあり、ブラウザはそれらすべてをスタイル設定、レイアウト、ペイント、ガベージコレクションする必要がありました。新しいレンダラーはCanvas上に同じデータを描画します。列から数値を直接読み取り、ピクセルを描画します。フレームごとに要素やオブジェクトはありません。これにより、ズーム、リセット、スクロールが瞬時に感じられます。なぜなら、それらのいずれもDOMに触れなくなったからです。フレイムグラフはフレームで構成されています。フレイムグラフのレンダリングに関して、SVGとCanvasのHTML要素の違いを調べてみましょう。SVGとDOMはブラウザのレティンドモードAPIですが、CanvasはイミディエイトモードAPIです。これはどういう意味ですか?レティンドモードでは、ブラウザに要素のツリーが渡され、ブラウザがそれを所有します。ブラウザは、各要素のレイアウト変更とペイントを担当します。そして、それらはツリー内に存在するため、いずれかの変更は、ツリー内の他の要素であっても、スタイルとレイアウトの再計算を強制します。つまり、ブラウザは描画したもののモデルを保持し、それをあなたのために再レンダリングします。これは静的なドキュメントにはうまく機能しますが、本質的に数千の色付きフレームとテキストで構成される複雑なコンポーネント(フレイムグラフなど)にとっては、多くのブックキーピングになります。(Reactを書いている場合、これはあなたがすでに生きている世界です。UIを再記述すると、Reactはそれをあなたと同期したDOMツリーを保持します。これは、上にフレンドリーな顔を付けたレティンドモードです。)イミディエイトモードAPIにはツリーがありません。代わりに、描画コンテキストがあり、fillRectやfillTextのような描画コマンドを発行できます。ブラウザはそれらをピクセルに変換して忘れ去ります。スタイル設定するノードやレイアウトするボックスがないため、ガベージコレクタのために残るものは何もありません。ブラウザのバックグラウンドタスクである、誰も使用していないメモリの回収が排除されます。特に私たちにとって、Canvasでフレイムグラフをレンダリングするコストは、ブラウザが記憶する必要のあるフレーム数にスケーリングするものから、実際に描画する数にスケーリングするものへと変化します。フレイムグラフはSQLクエリになりました。クエリとデータベースをRustで書き直したとき、フレイムグラフはSQLで呼び出すことができる合成可能な関数の出力になりました。クエリはサンプルをウォークし、ツリーを構築し、各フレームの位置と幅を計算し、レンダリング準備完了の列指向Arrowとしてストリームします。列指向Arrowに慣れていない場合、それは基本的にデータがテーブルとして返されることを意味します。複数のオブジェクトがそれぞれフレームのx、幅、深さを一緒に含むのではなく、各フィールドを独自の配列に保持します。すべてのx位置が1つの配列に、すべての幅が別の配列に、すべての深さが別の配列にあります。これで、フレイムグラフのレンダリングは、数千回同じ小さなジョブを実行することを意味します。1フレームの左端、その幅、その行を取得し、それを描画し、次へ進みます。すべての左端がすでに単一の配列にまとまっているため、それらを読み取ることは、オブジェクトを開いて必要な1つの数値を取り出すのではなく、リストをまっすぐに下っていくことになります。列指向Arrowレコードは、Arrow独自のネットワークトランスポートであるArrow Flightを介して、そのIPC形式で送られてきます。これのクールな点は、Arrowのワイヤー上のレイアウトがメモリ上のレイアウトと同一であるため、データが到着すると、tableFromIPC関数は、解析したり新しいものにコピーしたりすることなく、簡単に使用可能なテーブルを提供してくれます。これは「ゼロコピー」であり、ワイヤーを介してデータを取得しレンダリングする際に、常に遵守するように最善を尽くした戦略です。これを行うことで、フレイムグラフツリーが構築され、レイアウトが完了してからデータが返されるため、高速なCanvas描画の準備が整いました。Canvasへの描画位置が事前に計算されているため、描画ループは非常に単純です。ペイントする必要のある各フレームについて、Arrowの列から直接フィールドを読み取り、Canvas座標にマッピングします。const x = (valueOffset / total) * width; const y = depth * ROW_HEIGHT; ctx.fillStyle = colourFor(frame); ctx.fillRect(x, y, (cumulative / total) * width, ROW_HEIGHT); ctx.fillText(truncate(name, availableWidth), x + PADDING, y + textBaseline);ここでゼロコピーはその真価を発揮します。単に列を読み取るだけで、そのフレームについて知る必要があるすべてが得られます。JavaScriptオブジェクトやSVG要素を各フレームごとに構築する追加時間を費やす必要はもうありません。それらはブラウザやガベージコレクタによって追跡および回収される必要がありました。また、以前のフレイムグラフ実装にあったビューポートカリング戦略も維持しました。つまり、画面に表示できるものだけを描画します。ここでの違いは、カリング戦略がDOMノードをマウントする数ではなく、fillRect呼び出しの数を決定することです。プロファイルに50,000フレームがあり、そのうち800フレームだけが表示領域内にある場合、50,000ではなく約800回の描画呼び出しを発行します。スクロールとズームはどうですか?フレイムグラフを高速に描画することは、仕事の半分にすぎません。かつて最も苦痛だったインタラクションは、最初のペイントの後でした。深いスタックのスクロール、ホットパスへのズーム、リセット。それらは今、2つの理由で安価です。どちらもCanvasがこのジョブにより適していることに起因します。第一に、私たちはグラフ全体を一度に描画しません。フレイムグラフは画面よりもはるかに高くなる可能性があり、Canvasは無限に高くすることはできません。そのため、表示ウィンドウよりも少し大きいサイズに設定します。スクロールするとき、ほとんどの場合、再描画しません。すでにペイントされたピクセルを、ウェブページがスクロールするのと同じようにスライドさせ、マージンを越えたときにだけ新しいスライスを再ペイントします。第二に、ズームは何も再構築しません。各フレームの位置と幅は、クエリで一度計算されているため、ズームは単にそれらの固定された数値のより狭いスライスをCanvasに再投影するだけで、リセットは完全な範囲を元に戻します。2度目に歩くツリーはなく、再び測定するものもありません。次に進む前に、Canvasレンダラーがライブで実行されている様子を示します。測定したもの内部的には、私たちが受け取ったフィードバックは、新しいプロファイラーが「より速く感じられる」ということでした。これは、受け取るのに最も良い種類のフィードバックであり、ブログ投稿に入れるのに最も悪い種類のフィードバックです。何が速くなったのか、クエリまたはレンダリングのどちらで勝利が得られたのか、あるいは単にフレイムグラフが今回小さいだけなのかは分かりません。そこで、3つの方法で測定しました。ユーザーが実際に感じるものに対するブラウザ自動化、レンダラーを分離するためのフィクスチャ再生、およびブラウザが時間を費やす場所を確認するためのChrome DevToolsトレースです。最も信頼している数値は、フィクスチャ再生から得られます。1つのプロファイル(2日間のオンCPUクエリ、深く大規模なもの)の正確なArrowレスポンスを保存し、次に同じバイトに対して古いSVGレンダラーと新しいCanvasレンダラーをマウントします。