Upgrade to Pro — share decks privately, control downloads, hide ads and more …

お絵かきツールのパフォーマンスチューニング

nontan-rh
April 14, 2017

 お絵かきツールのパフォーマンスチューニング

pixiv Sketch WebGLお絵かき機能のざっくりとしたチューニングの話

nontan-rh

April 14, 2017
Tweet

More Decks by nontan-rh

Other Decks in Programming

Transcript

  1. お絵かきツールの
    パフォーマンスチューニング
    ~ 60FPSのために ~

    View Slide

  2. 自己紹介
    ● 名前: 磯崎 希 (のんたん)
    ● 職業: アルバイト
    ● 出身: ネイティブ畑
    ○ C++, Swift, など...
    ○ Javascript歴は2ヶ月くらいです
    ● 仕事内容: iOS, Android, Webのお絵かき機能を担当しています
    ○ OpenGLまわりをゴリゴリやってます

    View Slide

  3. 今回の内容
    ● pixiv Sketchのお絵かき機能の総合的なパフォーマンス改善の話をします
    ○ WebGLだけじゃないです
    ● 改善例を列挙します
    ● 基本的な話が多いです

    View Slide

  4. 準備

    View Slide

  5. パフォーマンス改善の手順
    1. ちゃんと測る
    2. ちゃんとボトルネックを潰す
    3. 1.に戻る

    View Slide

  6. パフォーマンス改善の手順(つづき)
    ● 「ちゃんと測る」がとても難しい
    ○ クロック周波数が変化する
    ○ 裏のプロセスの状況によって変化する
    ○ 手でドローの情報を入力するのでムラがある
    ○ ブラウザが起動してからどれくらい時間が経ったかでムラがある
    ○ 何回も測って平均を取るべき
    ○ 改善したかどうか検定するべき

    View Slide

  7. パフォーマンスの指標
    ● FPSで評価するわけではない
    ○ マウスイベントが入ってきたタイミングで処理をしている
    ○ お絵かきツールでフレーム落ちは末期的
    ○ フレーム落ちが存在しているかどうかだけ見る
    ● 代わりに1フレーム当たり処理時間の割合を見る
    ○ 改善目的の処理が他の主要な処理の何 %の時間を要しているか

    View Slide

  8. パフォーマンスの指標(Chromeの場合 その1)

    View Slide

  9. パフォーマンスの指標(Chromeの場合 その2)

    View Slide

  10. パフォーマンスの指標(Edgeの場合 その1)

    View Slide

  11. パフォーマンスの指標(Edgeの場合 その2)

    View Slide

  12. 実例

    View Slide

  13. GCが......?
    ● ブラウザによって差が大きいが......

    View Slide

  14. ストロークデータ生成(before)
    const BSplineCurve = {
    refine: (points: [number, number][]): [number, number][] => {
    const refined = [];
    if (points.length - 1 >= 1) {
    for (let i = 1; i <= points.length - 1; i++) {
    refined
    .push(points[i - 1]);
    refined
    .push([(points[i - 1][0] + points[i][0]) * 0.5, (points[i - 1][1] + points[i][1]) * 0.5]);
    }
    }
    if (points.length > 0) {
    refined.push(points[points.length - 1]);
    }
    return refined;
    },
    };

    View Slide

  15. ストロークデータ生成(after)
    const BSplineCurve = {
    refine: (dst: number[], src: number[], num): void => {
    for (let i = 1; i <= num - 1; i++) {
    const srcBase = (i - 1) * 2;
    const dstBase = (i - 1) * 4;
    dst[dstBase + 0] = src[srcBase];
    dst[dstBase + 1] = src[srcBase + 1];
    dst[dstBase + 2] = (src[srcBase] + src[srcBase + 2]) * 0.5;
    dst[dstBase + 3] = (src[srcBase + 1] + src[srcBase + 3]) * 0.5;
    }
    dst[(num - 1) * 4] = src[(num - 1) * 2];
    dst[((num - 1) * 4) + 1] = src[((num - 1) * 2) + 1];
    return ((num - 1) * 2) + 1;
    },
    };

    View Slide

  16. ストロークデータ生成(結果)

    View Slide

  17. レイヤー合成が......?

    View Slide

  18. レイヤー合成範囲
    ● 線を引いている途中、1フレームの間に変化している部分は一部のみ
    ● 具体的には線の先っぽのみ

    View Slide

  19. レイヤー合成範囲
    ● 変更があり得る場所だけを更新すれば良い
    ● 更新範囲を覆う縦横が水平垂直の矩形を算出して、その部分だけ更新する

    View Slide

  20. レイヤー合成範囲(補足)
    ● B-スプライン曲線は凸包性がある
    ○ 曲線は曲線を作るのに用いた制御点の凸包に収まる
    ● 合成範囲は各制御点のx, yそれぞれの最小、最大を求めるだけで計算できる

    View Slide

  21. レイヤー合成範囲(コード)
    // dirtyRect: 更新すべき矩形
    gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Uint8Array([
    dirtyRect.left, dirtyRect.top,
    dirtyRect.right, dirtyRect.top,
    dirtyRect.right, dirtyRect.bottom,
    dirtyRect.left, dirtyRect.bottom,
    ]), gl.DYNAMIC_DRAW);
    gl.enableVertexAttribArray
    (this.vertexLocation
    );
    gl.vertexAttribPointer
    (this.vertexLocation
    , 2, gl.FLOAT, false, 0, 0);
    gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);

    View Slide

  22. gl.bufferData, gl.bufferSubDataが重い......
    ● 前に述べた矩形(dirtyRect)の転送

    View Slide

  23. Uniform化
    // 初期化(一回だけ走る)
    const rect = new Float32Array([0, 0, 0, 1, 1, 1, 1, 0]);
    gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, rect, gl.STATIC_DRAW);
    // 描画
    gl.uniform4f(dirtyRectLocation
    , dirtyRect.left, dirtyRect.top, dirtyRect.right, dirtyRect.bottom);
    gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
    // シェーダ(vertex shader)
    attribute vec2 inVertex
    ;
    uniform vec4 dirtyRect
    ;
    gl_Position = vec4((vec2(1.) - inVertex) * dirtyRect.xy + inVertex * dirtyRect.zw, 0., 1.);

    View Slide

  24. レイヤー合成最適化の結果

    View Slide

  25. gl.readPixels, IOは重い......
    ● Sketchで扱うデータはindexedDBに自動保存される
    ○ 細かいストロークを連続したときに問題に

    View Slide

  26. バックアップを遅延させる
    ● ストロークが終わってから1秒間バックアップを遅延させる
    ○ 1秒間の間に次のストロークが始まったらバックアップをキャンセル
    ○ 次のストロークに関しても同様に

    View Slide

  27. バックアップを遅延させる(コード)
    class BackupScheduler {
    schedule = () => {
    if (this.timeoutId) {
    clearTimeout
    (this.timeoutId);
    this.timeoutId = null;
    }
    this.timeoutId = setTimeout(() => {
    if (this.isDrawing) {
    this.timeoutId = null;
    return;
    }
    // gl.readPixels(...);
    // indexedDBに保存
    }, 1000);
    };
    }

    View Slide

  28. バックアップを遅延させる(結果)

    View Slide

  29. グラフが紫色に......
    ● 内訳は「レンダリング」
    ○ WebGLでのレンダリングの時間ではなく、ブラウザ側の再描画の時間

    View Slide

  30. グラフが紫色に......
    ● 原因はcanvasにメニューが覆いかぶさっていたこと
    ○ 下のcanvasが再描画されると、その上にかかっている要素に再描画フラグが立つ (?)
    ● メニューとcanvasの重なりをなくして改善

    View Slide

  31. 結果(Edge)

    View Slide

  32. 結果(Chrome)

    View Slide

  33. 描き出しが重い......

    View Slide

  34. ReactのsetStateが重かった
    ● ブラウザによって差が大きい
    ● ストローク中はsetStateを呼ばないようにする
    ○ Reactに頼らない
    ○ どうしても使いたければ、 setStateをストロークの終わりまで遅延させる

    View Slide

  35. 以上です
    御清聴ありがとうございました

    View Slide