Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

準備

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

実例

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

ストロークデータ生成(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; }, };

Slide 15

Slide 15 text

ストロークデータ生成(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; }, };

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

レイヤー合成範囲(コード) // 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);

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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.);

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

バックアップを遅延させる(コード) 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); }; }

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

結果(Edge)

Slide 32

Slide 32 text

結果(Chrome)

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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