Slide 1

Slide 1 text

Deopt ExplorerでWebアプリの パフォーマンスを改善しよう! in Cybozu Frontrend Day 2023/06/30 BaHo @b4h0_c4t 1

Slide 2

Slide 2 text

自己紹介 - 名前 - BaHo - 所属 - フロントエンドエキスパートチーム - 興味関心 - Webフロントエンド - ゲーム - JRPG とかアクション系が好き 2

Slide 3

Slide 3 text

Deopt Explorer って何? V8 のトレースログをもとにインラインキャッシングに適していないコードを発見・可視化し てくれる VS Code 拡張です。 ※V8 : Chrome や Node.js で使われている JavaScript エンジン 3

Slide 4

Slide 4 text

🤔 4

Slide 5

Slide 5 text

インラインキャッシングって何だよ 以前に実行したコードのキャッシュを利用して次回以降の実行速度を高速化する手法 5 const sum = (a, b) => { return a + b; }; sum(1, 2); // 3 sum(2, 3); // 5 JavaScript ランタイムでは、コードを実行する際に対象コードを実行可能なバイ ナリに変換してメモリへ展開し、それを実行します。 左の例では、sum(1, 2); を実行した際にメモリへ展開された sum() 関数を流用 して sum(2, 3) の処理が実行されるため、 sum(1, 2) と比較して sum(2, 3) の実 行は高速になります。 ※「implicit any だ!○せ!」と思ったそこのあなた、これは JavaScriptです。

Slide 6

Slide 6 text

インラインキャッシングができるとき・できないとき 実行バイナリ上で同様の計算処理が走るコードはできる 6 const sum = (a, b) => { return a + b; }; sum(1, 2); // 3 sum(2, 3); // 5 IC! const sum = (a, b) => { return a + b; }; sum(1, 2); // 3 sum(2, "3"); // "23" not IC! どちらも算術的な加算を実行しているのでイ ンラインキャッシングが有効 sum(1, 2) は算術的な加算だが sum(2, “3”) は文字列結合のため、 インラインキャッシングは無効 普段 TypeScript を利用しているのならプリミティブな値でこの現象を踏む人は少ないと思います

Slide 7

Slide 7 text

Q. TS でインラインキャッシングできないことあるの? A. ありまぁす! 主にオブジェクトを取り扱うときに発生します。 Q. TSならオブジェクトにも型をつけられるよね? 実は JavaScript ランタイムは内部でオブジェクトの型を保持していて、TSのそれとはい くつかの不整合があります。 7 ※string | number とか Optional みたいな型定義でも発生するので実際は割とどこでも発生します

Slide 8

Slide 8 text

Hidden Class JavaScript ランタイムが持つ、任意のオブジェクト型(のようなもの) プログラムを実行しながら動的に生成されます。 特定の関数に対しての引数となるオブジェクトのプロパティとその型およびレイアウ ト(オフセット)を保持しています。 このオフセットのおかげでオブジェクトプロパティへのアクセスが高速化されていま す。が、今回とは別のお話なので割愛 8 ※V8 の実装では Map と呼ばれています。

Slide 9

Slide 9 text

Hidden Class の生成 プログラム内で関数が呼び出されると、呼び出された引数をもとに Hidden Classが生 成されます。 次回以降の呼び出しは、生成された Hidden Class をもとにインラインキャッシシングが 可能か検査が行われます。 9 const sum = (x) => { return x.a + x.b; }; sum({ a: 1, b: 2 }); // 3 sum({ a: 2, b: 3 }); // 5 map 0x38a5002079c9 extends Object { a: unknown; b: unknown; }

Slide 10

Slide 10 text

Hidden Class の追加 引数オブジェクトの形が変わると、そのオブジェクトに対する Hidden Class の追加が行 われます。 10 const sum = (x) => { if (x.c) return x.a + x.b + x.c; return x.a + x.b; }; sum({ a: 1, b: 2 }); // 3 HiddlenClass1 sum({ a: 1, b: 2, c: 3 }); map HiddenClass1 extends Object { a: unknown; b: unknown; } map HiddenClass2 extends Object { a: unknown; b: unknown; c: unknown; // Added }

Slide 11

Slide 11 text

Hidden Class のオフセット オブジェクトプロパティの定義順が変わっても新しい Hidden Class が生成されます。 11 const sum = (x) => { return x.a + x.b; }; sum({ a: 1, b: 2 }); // 3 HiddlenClass1 sum({ b: 1, a: 2 }); // 3 HiddenClass2 map HiddenClass1 extends Object { a: unknown; b: unknown; } map HiddenClass2 extends Object { b: unknown; a: unknown; }

Slide 12

Slide 12 text

つまり... TSでの型定義に関わらず、Hidden Class が変わると (内部の型が変わるため) イン ラインキャッシングができない!となります ちなみに、 任意の引数オブジェクトに対して Hidden Class が複数存在している状態(性質)を Polymorphic、その数がより多いものを Megamorphic と呼びます。 12 ※ポリモーフィックです。どこかできいたことがありますね。

Slide 13

Slide 13 text

Deopt Explorer の話に戻ります 13

Slide 14

Slide 14 text

Deopt Explorer って何? V8 のトレースログをもとにインラインキャッシングに適していないコードを発見・可視化し てくれる VS Code 拡張です。 ※V8 : Chrome や Node.js で使われている JavaScript エンジン 14

Slide 15

Slide 15 text

🤓💡 15

Slide 16

Slide 16 text

何を言っているか分かりましたね?(圧) 16

Slide 17

Slide 17 text

ここまで前座 17

Slide 18

Slide 18 text

実際に Explore してみよう! - 必要なもの - お手持ちの PC - VS Code (Market Place から Deopt Explrer を追加しておこう) - Google Chrome (MS Edge でも良いらしいけど未検証 ) - 手順 - Chrome で検証したいWebアプリのトレースログを取得する - 取得した v8-log を Deopt Explorer で読み込む 18 chrome --no-sandbox --js-flags=--log-deopt,--log-i c,--log-maps,--log-maps-detail s,--log-internal-timer-events, --prof,... <トレースしたいURL> alias chrome="/path/to/Google\ Chrome"

Slide 19

Slide 19 text

以上! 19

Slide 20

Slide 20 text

とても簡単なデモ 20

Slide 21

Slide 21 text

プログラムの解説 21 Document function f(x, y) { if (x <= y) { return { x, y }; } else { return { y, x }; } } function g(p) { const x = p.x; // polymorphic } for (let i = 0; i < 1000; i++) g(f(0, 1)); g(f(1, 0)); index.html polymorphicExample.js 関数 f() の引数が {x, y} か {y, x} として呼び出されているだけのページ

Slide 22

Slide 22 text

Deopt Explorerにトレースログを流した結果 左の「ICs」に polymorphicExample.js が ある! 「Polymorphic」の注釈がつ いていますね。 22

Slide 23

Slide 23 text

強調箇所をピークしてみる Uninitialized => Monomorphic x => Polymorphic x と Hidden Class が遷移している ことがわかります。 また、それぞれに対応した Map も参照されていますね。 23

Slide 24

Slide 24 text

Map を覗く 1つ目(左)が {x, y} なのに対して、2つ目が {y, x} になっているのがわかります。 その Mapがどこで追加されたのかも Added by *** から追跡できます。 24

Slide 25

Slide 25 text

プログラムを修正する 修正箇所がわかったので早速直しましょう 25 function f(x, y) { if (x <= y) { return { x, y }; } else { return { y, x }; } } function g(p) { const x = p.x; // polymorphic } for (let i = 0; i < 1000; i++) g(f(0, 1)); g(f(1, 0)); polymorphicExample.js function f(x, y) { return { x, y }; } function g(p) { const x = p.x; // polymorphic } for (let i = 0; i < 1000; i++) g(f(0, 1)); g(f(1, 0)); polymorphicExample.js ※身も蓋も無い修正

Slide 26

Slide 26 text

トレードオフ 何でもかんでも引数を固定すれば良いわけではない - Optional な引数を辞めるために 引数ありなしの2パターンの関数を作ってしまうと その分メモリが圧迫されます。 - Webアプリにとってはオブジェクトへのアクセスよりも描画にかかるコストの方が圧 倒的に高いため、どちらかを取らなければいけない場面では描画側の懸念事項を 優先した方が良い 26 要はバランス とはいえ、この観点から修正できるプログラムは多いですし、積極的にリファクタリングし てみてください

Slide 27

Slide 27 text

意外と知られていないパフォーマンスチューニング 実は引数に気を使うことでコードの実行速度を改善することができると学べたかと思い ます。 Deopt Explorer というツールの解説を通して、こういった観点をみなさんに持ってもらえ たなら幸いです。 27 パフォーマンス改善に栄光あれ!

Slide 28

Slide 28 text

終わり 28