$30 off During Our Annual Pro Sale. View Details »

Deopt Explorer で Web アプリのパフォーマンスを改善しよう!

Deopt Explorer で Web アプリのパフォーマンスを改善しよう!

Webアプリケーションのパフォーマンス最適化には様々な観点や手法があります。
Deopt Explorer では、普段あまり気にされることのない V8(JavaScript ランタイム)の挙動に関心を置いたパフォーマンス改善の種を見つけることができます。
この資料では、Deopt Explorer を使って Web アプリケーションのパフォーマンスを改善するための事前知識および手法について紹介しています。

b4h0-c4t

July 18, 2023
Tweet

More Decks by b4h0-c4t

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

  4. 🤔
    4

    View Slide

  5. インラインキャッシングって何だよ
    以前に実行したコードのキャッシュを利用して次回以降の実行速度を高速化する手法
    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です。

    View Slide

  6. インラインキャッシングができるとき・できないとき
    実行バイナリ上で同様の計算処理が走るコードはできる
    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 を利用しているのならプリミティブな値でこの現象を踏む人は少ないと思います

    View Slide

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

    View Slide

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

    View Slide

  9. 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;
    }

    View Slide

  10. 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
    }

    View Slide

  11. 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;
    }

    View Slide

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

    View Slide

  13. Deopt Explorer の話に戻ります
    13

    View Slide

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

    View Slide

  15. 🤓💡
    15

    View Slide

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

    View Slide

  17. ここまで前座
    17

    View Slide

  18. 実際に 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"

    View Slide

  19. 以上!
    19

    View Slide

  20. とても簡単なデモ
    20

    View Slide

  21. プログラムの解説
    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} として呼び出されているだけのページ

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  25. プログラムを修正する
    修正箇所がわかったので早速直しましょう
    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
    ※身も蓋も無い修正

    View Slide

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

    View Slide

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

    View Slide

  28. 終わり
    28

    View Slide