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 <kjlubick@google.com>
This commit is contained in:
parent
c41ae2a3cf
commit
1a4107a32a
21
demos.skia.org/demos/path_performance/garbage.svg
Normal file
21
demos.skia.org/demos/path_performance/garbage.svg
Normal file
@ -0,0 +1,21 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
|
||||
<path
|
||||
fill="#3A5"
|
||||
d="
|
||||
M 7, 50
|
||||
a 43,43 0 1,0 86,0
|
||||
a 43,43 0 1,0 -86,0
|
||||
"
|
||||
/>
|
||||
<path
|
||||
d="
|
||||
M 53, 23
|
||||
a 6,6 0 1,0 12,0
|
||||
a 6,6 0 1,0 -12,0
|
||||
"
|
||||
/>
|
||||
<path d="M36,36c5,0,3,2,8-1c1,2,1,3,3,2c3,0-6,7-3,8c-4-2-9,2-14-2c4-3,4-4,5-7c5,0,8,2,12,1"/>
|
||||
<path fill="#000" d="M34,29h31c2,5,7,10,7,16l-8,1l8,1l-3,31l-5,-18l-11,18l5-34l-3-8z"/>
|
||||
<path stroke-width="2" d="M27,48h23M28,49h21l-3,28h-14l-4,-28h5l3,28h3v-28h5l-2,28m3-4h-13m-1-5h16m0-5h-16m-1-5h18m0-5h-19"/>
|
||||
<path stroke="#F00" stroke-width="1"/>
|
||||
</svg>
|
After Width: | Height: | Size: 644 B |
97
demos.skia.org/demos/path_performance/index.html
Normal file
97
demos.skia.org/demos/path_performance/index.html
Normal file
@ -0,0 +1,97 @@
|
||||
<!DOCTYPE html>
|
||||
<title>CanvasKit Path Rendering Performance Demo</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<style>
|
||||
html, body {
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
#result-container {
|
||||
border: 1px dashed grey;
|
||||
height: 500px;
|
||||
width: 500px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
canvas {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
object {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
height: 500px;
|
||||
width: 500px;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
width: 33%;
|
||||
}
|
||||
td {
|
||||
padding: 12px;
|
||||
color: #555;
|
||||
font-style: italic;
|
||||
height: 80px;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
<h1>CanvasKit Path Rendering Performance Demo</h1>
|
||||
<p>NOTE: this demo currently only works in chromium-based browsers, where
|
||||
<a href="https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas#Browser_compatibility">
|
||||
Offscreen Canvas
|
||||
</a>
|
||||
is supported.
|
||||
</p>
|
||||
|
||||
|
||||
<h2>1. Choose a rendering method</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>
|
||||
<input type="radio" id="SVG-input" name="rendermethod" checked>
|
||||
<label for="SVG-input">SVG</label>
|
||||
</th>
|
||||
<th>
|
||||
<input type="radio" id="Path2D-input" name="rendermethod">
|
||||
<label for="Path2D-input">
|
||||
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Path2D">Path2D API</a>
|
||||
</label>
|
||||
</th>
|
||||
<th>
|
||||
<input type="radio" id="CanvasKit-input" name="rendermethod">
|
||||
<label for="CanvasKit-input">CanvasKit</label>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td id="SVG-fps">Choose this rendering method to collect data on its performance...</td>
|
||||
<td id="Path2D-fps">Choose this rendering method to collect data on its performance...</td>
|
||||
<td id="CanvasKit-fps">Choose this rendering method to collect data on its performance...</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2>2. View the result</h2>
|
||||
<div id="result-container">
|
||||
<!-- Arbitrary svg for testing. Source: https://dev.w3.org/SVG/tools/svgweb/samples/svg-files-->
|
||||
<object type="image/svg+xml" id="svg">
|
||||
Garbage pictograph
|
||||
</object>
|
||||
<canvas id="Path2D-canvas" height=500 width=500></canvas>
|
||||
<canvas id="CanvasKit-canvas" height=500 width=500></canvas>
|
||||
</div>
|
||||
</body>
|
||||
<script type="text/javascript" src="https://particles.skia.org/static/canvaskit.js"></script>
|
||||
<script type="text/javascript" src="shared.js"></script>
|
||||
<script type="text/javascript" src="main.js"></script>
|
106
demos.skia.org/demos/path_performance/main.js
Normal file
106
demos.skia.org/demos/path_performance/main.js
Normal file
@ -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;
|
||||
}
|
140
demos.skia.org/demos/path_performance/shared.js
Normal file
140
demos.skia.org/demos/path_performance/shared.js
Normal file
@ -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();
|
||||
}
|
||||
}
|
54
demos.skia.org/demos/path_performance/worker.js
Normal file
54
demos.skia.org/demos/path_performance/worker.js
Normal file
@ -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);
|
Loading…
Reference in New Issue
Block a user