From 1a4107a32a51363bd438eda463b6b3dad63bfbe1 Mon Sep 17 00:00:00 2001 From: Elliot Evans Date: Tue, 11 Aug 2020 13:55:58 -0600 Subject: [PATCH] Add path rendering performance demo to demos.skia.org In this demo the user may choose between one of three path rendering methods 1. SVG 2. Canvas2D Path2D API 3. CanvasKit SVGs is animated using css transforms on the main thread, while Canvas2D and CanvasKit are animated in a worker using OffscreenCanvas. While the user views the result of the rendering, the demo collects framerate data and displays it so the user may compare the performance of the three methods. Change-Id: I8cd6e079bab8815614e09a276cfe78bee9557fda Reviewed-on: https://skia-review.googlesource.com/c/skia/+/309327 Reviewed-by: Kevin Lubick --- .../demos/path_performance/garbage.svg | 21 +++ .../demos/path_performance/index.html | 97 ++++++++++++ demos.skia.org/demos/path_performance/main.js | 106 +++++++++++++ .../demos/path_performance/shared.js | 140 ++++++++++++++++++ .../demos/path_performance/worker.js | 54 +++++++ 5 files changed, 418 insertions(+) create mode 100644 demos.skia.org/demos/path_performance/garbage.svg create mode 100644 demos.skia.org/demos/path_performance/index.html create mode 100644 demos.skia.org/demos/path_performance/main.js create mode 100644 demos.skia.org/demos/path_performance/shared.js create mode 100644 demos.skia.org/demos/path_performance/worker.js diff --git a/demos.skia.org/demos/path_performance/garbage.svg b/demos.skia.org/demos/path_performance/garbage.svg new file mode 100644 index 0000000000..7ccc45a1b0 --- /dev/null +++ b/demos.skia.org/demos/path_performance/garbage.svg @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/demos.skia.org/demos/path_performance/index.html b/demos.skia.org/demos/path_performance/index.html new file mode 100644 index 0000000000..7ebbb8227a --- /dev/null +++ b/demos.skia.org/demos/path_performance/index.html @@ -0,0 +1,97 @@ + +CanvasKit Path Rendering Performance Demo + + + + + + + +

CanvasKit Path Rendering Performance Demo

+

NOTE: this demo currently only works in chromium-based browsers, where + + Offscreen Canvas + + is supported. +

+ + +

1. Choose a rendering method

+ + + + + + + + + + + +
+ + + + + + + + +
Choose this rendering method to collect data on its performance...Choose this rendering method to collect data on its performance...Choose this rendering method to collect data on its performance...
+ +

2. View the result

+
+ + + Garbage pictograph + + + +
+ + + + diff --git a/demos.skia.org/demos/path_performance/main.js b/demos.skia.org/demos/path_performance/main.js new file mode 100644 index 0000000000..d618bfb9cb --- /dev/null +++ b/demos.skia.org/demos/path_performance/main.js @@ -0,0 +1,106 @@ +const DEFAULT_METHOD = 'SVG'; + +const worker = new Worker('worker.js'); + +const svgObjectElement = document.getElementById('svg'); +document.getElementById('svg').addEventListener('load', () => { + + const svgElement = svgObjectElement.contentDocument; + const svgData = svgToPathStringAndFillColorPairs(svgElement); + + // Send svgData and transfer an offscreenCanvas to the worker for Path2D and CanvasKit rendering + const path2dCanvas = + document.getElementById('Path2D-canvas').transferControlToOffscreen(); + worker.postMessage({ + svgData: svgData, + offscreenCanvas: path2dCanvas, + type: 'Path2D' + }, [path2dCanvas]); + const canvasKitCanvas = + document.getElementById('CanvasKit-canvas').transferControlToOffscreen(); + worker.postMessage({ + svgData: svgData, + offscreenCanvas: canvasKitCanvas, + type: 'CanvasKit' + }, [canvasKitCanvas]); + + // The Canvas2D and CanvasKit rendering methods are executed in a web worker to avoid blocking + // the main thread. The SVG rendering method is executed in the main thread. SVG rendering is + // not in a worker because it is not possible - the DOM cannot be accessed from a web worker. + const svgAnimator = new Animator(); + svgAnimator.renderer = new SVGRenderer(svgObjectElement); + switchRenderMethodCallback(DEFAULT_METHOD)(); + + // Listen to framerate reports from the worker, and update framerate text + worker.addEventListener('message', ({ data: {renderMethod, framesCount, totalFramesMs} }) => { + const fps = fpsFromFramesInfo(framesCount, totalFramesMs); + let textEl; + if (renderMethod === 'Path2D') { + textEl = document.getElementById('Path2D-fps'); + } + if (renderMethod === 'CanvasKit') { + textEl = document.getElementById('CanvasKit-fps'); + } + textEl.innerText = `${fps.toFixed(2)} fps over ${framesCount} frames`; + }); + // Update framerate text every second + setInterval(() => { + if (svgAnimator.framesCount > 0) { + const fps = fpsFromFramesInfo(svgAnimator.framesCount, svgAnimator.totalFramesMs); + document.getElementById('SVG-fps').innerText = + `${fps.toFixed(2)} fps over ${svgAnimator.framesCount} frames`; + } + }, 1000); + + document.getElementById('SVG-input') + .addEventListener('click', switchRenderMethodCallback('SVG')); + document.getElementById('Path2D-input') + .addEventListener('click', switchRenderMethodCallback('Path2D')); + document.getElementById('CanvasKit-input') + .addEventListener('click', switchRenderMethodCallback('CanvasKit')); + + function switchRenderMethodCallback(switchMethod) { + return () => { + // Hide all renderer elements and stop svgAnimator + document.getElementById('CanvasKit-canvas').style.visibility = 'hidden'; + document.getElementById('Path2D-canvas').style.visibility = 'hidden'; + for (const svgEl of svgAnimator.renderer.svgElArray) { + svgEl.style.visibility = 'hidden'; + } + svgAnimator.stop(); + + // Show only the active renderer element + if (switchMethod === 'SVG') { + svgAnimator.start(); + for (const svgEl of svgAnimator.renderer.svgElArray) { + svgEl.style.visibility = 'visible'; + } + } + if (switchMethod === 'CanvasKit') { + document.getElementById('CanvasKit-canvas').style.visibility = 'visible'; + } + if (switchMethod === 'Path2D') { + document.getElementById('Path2D-canvas').style.visibility = 'visible'; + } + worker.postMessage({ switchMethod }); + }; + } +}); +// Add .data after the load listener so that the listener always fires an event +svgObjectElement.data = 'garbage.svg'; + +const EMPTY_SVG_PATH_STRING = 'M 0 0'; +const COLOR_WHITE = '#000000'; +function svgToPathStringAndFillColorPairs(svgElement) { + const pathElements = Array.from(svgElement.getElementsByTagName('path')); + return pathElements.map((path) => [ + path.getAttribute('d') ?? EMPTY_SVG_PATH_STRING, + path.getAttribute('fill') ?? COLOR_WHITE + ]); +} + +const MS_IN_A_SECOND = 1000; +function fpsFromFramesInfo(framesCount, totalFramesMs) { + const averageFrameTime = totalFramesMs / framesCount; + return (1 / averageFrameTime) * MS_IN_A_SECOND; +} diff --git a/demos.skia.org/demos/path_performance/shared.js b/demos.skia.org/demos/path_performance/shared.js new file mode 100644 index 0000000000..8c516ae7bb --- /dev/null +++ b/demos.skia.org/demos/path_performance/shared.js @@ -0,0 +1,140 @@ +// Returns an [x, y] point on a circle, with given origin and radius, at a given angle +// counter-clockwise from the positive horizontal axis. +function circleCoordinates(origin, radius, radians) { + return [ + origin[0] + Math.cos(radians) * radius, + origin[1] + Math.sin(radians) * radius + ]; +} + +// Animator handles calling and stopping requestAnimationFrame and keeping track of framerate. +class Animator { + framesCount = 0; + totalFramesMs = 0; + animating = false; + renderer = null; + + start() { + if (this.animating === false) { + this.animating = true; + this.framesCount = 0; + const frameStartMs = performance.now(); + + const drawFrame = () => { + if (this.animating && this.renderer) { + requestAnimationFrame(drawFrame); + this.framesCount++; + + const [x, y] = circleCoordinates([-70, -70], 50, this.framesCount/100); + this.renderer.render(x, y); + + const frameTimeMs = performance.now() - frameStartMs; + this.totalFramesMs = frameTimeMs; + } + }; + requestAnimationFrame(drawFrame); + } + } + + stop() { + this.animating = false; + } +} + +// The following three renderers draw a repeating pattern of paths. +// The approximate height and width of this repeated pattern is given by PATTERN_BOUNDS: +const PATTERN_BOUNDS = 600; +// And the spacing of the pattern (distance between repeated paths) is given by PATTERN_SPACING: +const PATTERN_SPACING = 70; + +class SVGRenderer { + constructor(svgObjectElement) { + this.svgObjectElement = svgObjectElement; + this.svgElArray = []; + // Create an SVG element for every position in the pattern + for (let xo = 0; xo < PATTERN_BOUNDS; xo += PATTERN_SPACING) { + for (let yo = 0; yo < PATTERN_BOUNDS; yo += PATTERN_SPACING) { + const clonedSVG = svgObjectElement.cloneNode(true); + this.svgElArray.push(clonedSVG); + svgObjectElement.parentElement.appendChild(clonedSVG); + } + } + } + + render(x, y) { + let i = 0; + for (let xo = 0; xo < PATTERN_BOUNDS; xo += PATTERN_SPACING) { + for (let yo = 0; yo < PATTERN_BOUNDS; yo += PATTERN_SPACING) { + this.svgElArray[i].style.transform = `translate(${x + xo}px, ${y + yo}px)`; + i++; + } + } + } +} + +class Path2dRenderer { + constructor(svgData, offscreenCanvas) { + this.data = svgData.map(([pathString, fillColor]) => [new Path2D(pathString), fillColor]); + + this.ctx = offscreenCanvas.getContext('2d'); + } + + render(x, y) { + const ctx = this.ctx; + + ctx.clearRect(0, 0, 500, 500); + + for (let xo = 0; xo < PATTERN_BOUNDS; xo += PATTERN_SPACING) { + for (let yo = 0; yo < PATTERN_BOUNDS; yo += PATTERN_SPACING) { + ctx.save(); + ctx.translate(x + xo, y + yo); + + for (const [path, fillColor] of this.data) { + ctx.fillStyle = fillColor; + ctx.fill(path); + } + ctx.restore(); + } + } + } +} + +class CanvasKitRenderer { + constructor(svgData, offscreenCanvas, CanvasKit) { + this.CanvasKit = CanvasKit; + this.data = svgData.map(([pathString, fillColor]) => [ + CanvasKit.MakePathFromSVGString(pathString), + CanvasKit.parseColorString(fillColor) + ]); + + this.surface = CanvasKit.MakeWebGLCanvasSurface(offscreenCanvas, null); + if (!this.surface) { + throw 'Could not make canvas surface'; + } + this.canvas = this.surface.getCanvas(); + + this.paint = new CanvasKit.SkPaint(); + this.paint.setAntiAlias(true); + this.paint.setStyle(CanvasKit.PaintStyle.Fill); + } + + render(x, y) { + const canvas = this.canvas; + + canvas.clear(this.CanvasKit.WHITE); + + for (let xo = 0; xo < PATTERN_BOUNDS; xo += PATTERN_SPACING) { + for (let yo = 0; yo < PATTERN_BOUNDS; yo += PATTERN_SPACING) { + canvas.save(); + canvas.translate(x + xo, y + yo); + + for (const [path, color] of this.data) { + this.paint.setColor(color); + canvas.drawPath(path, this.paint); + } + canvas.restore(); + } + } + this.surface.flush(); + } +} diff --git a/demos.skia.org/demos/path_performance/worker.js b/demos.skia.org/demos/path_performance/worker.js new file mode 100644 index 0000000000..6a3fd5da29 --- /dev/null +++ b/demos.skia.org/demos/path_performance/worker.js @@ -0,0 +1,54 @@ +importScripts('https://particles.skia.org/static/canvaskit.js'); +importScripts('shared.js'); + +const CanvasKitPromise = + CanvasKitInit({locateFile: (file) => 'https://particles.skia.org/static/'+file}); + +const path2dAnimator = new Animator(); +const canvasKitAnimator = new Animator(); +addEventListener('message', async ({ data: {svgData, offscreenCanvas, type, switchMethod} }) => { + // The worker expect to receive 2 messages after initialization: One with an offscreenCanvas + // for Path2D rendering, and one with an offscreenCanvas for CanvasKit rendering. + if (svgData && offscreenCanvas && type) { + if (type === 'Path2D') { + path2dAnimator.renderer = + new Path2dRenderer(svgData, offscreenCanvas); + } + if (type === 'CanvasKit') { + const CanvasKit = await CanvasKitPromise; + canvasKitAnimator.renderer = + new CanvasKitRenderer(svgData, offscreenCanvas, CanvasKit); + } + } + // The worker receives a "switchMethod" message whenever the user clicks a rendering + // method button on the web page. + if (switchMethod) { + // Pause other renderers and start correct renderer + canvasKitAnimator.stop(); + path2dAnimator.stop(); + + if (switchMethod === 'Path2D') { + path2dAnimator.start(); + } else if (switchMethod === 'CanvasKit') { + canvasKitAnimator.start(); + } + } +}); + +// Report framerates of Path2D and CanvasKit rendering back to main.js +setInterval(() => { + if (path2dAnimator.framesCount > 0) { + postMessage({ + renderMethod: 'Path2D', + framesCount: path2dAnimator.framesCount, + totalFramesMs: path2dAnimator.totalFramesMs + }); + } + if (canvasKitAnimator.framesCount > 0) { + postMessage({ + renderMethod: 'CanvasKit', + framesCount: canvasKitAnimator.framesCount, + totalFramesMs: canvasKitAnimator.totalFramesMs + }); + } +}, 1000);