pixiv Sketch WebGLお絵かき機能のざっくりとしたチューニングの話
お絵かきツールのパフォーマンスチューニング~ 60FPSのために ~
View Slide
自己紹介● 名前: 磯崎 希 (のんたん)● 職業: アルバイト● 出身: ネイティブ畑○ C++, Swift, など...○ Javascript歴は2ヶ月くらいです● 仕事内容: iOS, Android, Webのお絵かき機能を担当しています○ OpenGLまわりをゴリゴリやってます
今回の内容● pixiv Sketchのお絵かき機能の総合的なパフォーマンス改善の話をします○ WebGLだけじゃないです● 改善例を列挙します● 基本的な話が多いです
準備
パフォーマンス改善の手順1. ちゃんと測る2. ちゃんとボトルネックを潰す3. 1.に戻る
パフォーマンス改善の手順(つづき)● 「ちゃんと測る」がとても難しい○ クロック周波数が変化する○ 裏のプロセスの状況によって変化する○ 手でドローの情報を入力するのでムラがある○ ブラウザが起動してからどれくらい時間が経ったかでムラがある○ 何回も測って平均を取るべき○ 改善したかどうか検定するべき
パフォーマンスの指標● FPSで評価するわけではない○ マウスイベントが入ってきたタイミングで処理をしている○ お絵かきツールでフレーム落ちは末期的○ フレーム落ちが存在しているかどうかだけ見る● 代わりに1フレーム当たり処理時間の割合を見る○ 改善目的の処理が他の主要な処理の何 %の時間を要しているか
パフォーマンスの指標(Chromeの場合 その1)
パフォーマンスの指標(Chromeの場合 その2)
パフォーマンスの指標(Edgeの場合 その1)
パフォーマンスの指標(Edgeの場合 その2)
実例
GCが......?● ブラウザによって差が大きいが......
ストロークデータ生成(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;},};
ストロークデータ生成(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;},};
ストロークデータ生成(結果)
レイヤー合成が......?
レイヤー合成範囲● 線を引いている途中、1フレームの間に変化している部分は一部のみ● 具体的には線の先っぽのみ
レイヤー合成範囲● 変更があり得る場所だけを更新すれば良い● 更新範囲を覆う縦横が水平垂直の矩形を算出して、その部分だけ更新する
レイヤー合成範囲(補足)● B-スプライン曲線は凸包性がある○ 曲線は曲線を作るのに用いた制御点の凸包に収まる● 合成範囲は各制御点のx, yそれぞれの最小、最大を求めるだけで計算できる
レイヤー合成範囲(コード)// 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);
gl.bufferData, gl.bufferSubDataが重い......● 前に述べた矩形(dirtyRect)の転送
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.);
レイヤー合成最適化の結果
gl.readPixels, IOは重い......● Sketchで扱うデータはindexedDBに自動保存される○ 細かいストロークを連続したときに問題に
バックアップを遅延させる● ストロークが終わってから1秒間バックアップを遅延させる○ 1秒間の間に次のストロークが始まったらバックアップをキャンセル○ 次のストロークに関しても同様に
バックアップを遅延させる(コード)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);};}
バックアップを遅延させる(結果)
グラフが紫色に......● 内訳は「レンダリング」○ WebGLでのレンダリングの時間ではなく、ブラウザ側の再描画の時間
グラフが紫色に......● 原因はcanvasにメニューが覆いかぶさっていたこと○ 下のcanvasが再描画されると、その上にかかっている要素に再描画フラグが立つ (?)● メニューとcanvasの重なりをなくして改善
結果(Edge)
結果(Chrome)
描き出しが重い......
ReactのsetStateが重かった● ブラウザによって差が大きい● ストローク中はsetStateを呼ばないようにする○ Reactに頼らない○ どうしても使いたければ、 setStateをストロークの終わりまで遅延させる
以上です御清聴ありがとうございました