Kevin Lubick | 5443bb3 | 2020-05-01 14:16:27 -0400 | [diff] [blame] | 1 | <!-- This benchmark aims to accurately measure the time it takes for Skottie to load the JSON and |
| 2 | turn it into an animation, as well as the times for the first hundred frames (and, as a subcomponent |
| 3 | of that, the seek times of the first hundred frames). This is set to mimic how a real-world user |
| 4 | would display the animation (e.g. using clock time to determine where to seek, not frame numbers). |
| 5 | --> |
| 6 | <!DOCTYPE html> |
| 7 | <html> |
| 8 | <head> |
| 9 | <title>Skottie-WASM Perf</title> |
| 10 | <meta charset="utf-8" /> |
| 11 | <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> |
| 12 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 13 | <script src="/static/canvaskit.js" type="text/javascript" charset="utf-8"></script> |
| 14 | <style type="text/css" media="screen"> |
| 15 | body { |
| 16 | margin: 0; |
| 17 | padding: 0; |
| 18 | } |
| 19 | </style> |
| 20 | </head> |
| 21 | <body> |
| 22 | <main> |
| 23 | <button id="start_bench">Start Benchmark</button> |
| 24 | <br> |
| 25 | <canvas id=anim width=1000 height=1000 style="height: 1000px; width: 1000px;"></canvas> |
| 26 | </main> |
| 27 | <script type="text/javascript" charset="utf-8"> |
| 28 | const WIDTH = 1000; |
| 29 | const HEIGHT = 1000; |
| 30 | // We sample MAX_FRAMES or until MAX_SAMPLE_SECONDS has elapsed. |
| 31 | const MAX_FRAMES = 600; // ~10s at 60fps |
| 32 | const MAX_SAMPLE_MS = 30 * 1000; // in case something takes a while, stop after 30 seconds. |
| 33 | const LOTTIE_JSON_PATH = '/static/lottie.json'; |
| 34 | const ASSETS_PATH = '/static/assets/'; |
| 35 | (function() { |
| 36 | |
| 37 | const loadKit = CanvasKitInit({ |
| 38 | locateFile: (file) => '/static/' + file, |
Kevin Lubick | 4ad6b50 | 2020-05-21 10:51:35 -0400 | [diff] [blame^] | 39 | }); |
Kevin Lubick | 5443bb3 | 2020-05-01 14:16:27 -0400 | [diff] [blame] | 40 | |
| 41 | const loadLottie = fetch(LOTTIE_JSON_PATH).then((resp) => { |
| 42 | return resp.text() |
| 43 | }); |
| 44 | |
| 45 | const loadFontsAndAssets = loadLottie.then((jsonStr) => { |
| 46 | const lottie = JSON.parse(jsonStr); |
| 47 | const promises = []; |
| 48 | promises.push(...loadFonts(lottie.fonts)); |
| 49 | promises.push(...loadAssets(lottie.assets)); |
| 50 | return Promise.all(promises); |
| 51 | }); |
| 52 | |
| 53 | Promise.all([loadKit, loadLottie, loadFontsAndAssets]).then((values) => { |
| 54 | const [CanvasKit, json, externalAssets] = values; |
| 55 | console.log(externalAssets); |
| 56 | const assets = {}; |
| 57 | for (const asset of externalAssets) { |
| 58 | if (asset) { |
| 59 | assets[asset.name] = asset.bytes; |
| 60 | } |
| 61 | } |
| 62 | const loadStart = performance.now(); |
| 63 | const animation = CanvasKit.MakeManagedAnimation(json, assets); |
| 64 | const loadTime = performance.now() - loadStart; |
| 65 | |
| 66 | const duration = animation.duration() * 1000; |
| 67 | const bounds = {fLeft: 0, fTop: 0, fRight: WIDTH, fBottom: HEIGHT}; |
| 68 | |
| 69 | const surface = getSurface(CanvasKit); |
| 70 | if (!surface) { |
| 71 | console.error('Could not make surface', window._error); |
| 72 | return; |
| 73 | } |
| 74 | const canvas = surface.getCanvas(); |
| 75 | |
| 76 | document.getElementById('start_bench').addEventListener('click', () => { |
| 77 | const clearColor = CanvasKit.WHITE; |
| 78 | const frames = new Float32Array(MAX_FRAMES); |
| 79 | const seeks = new Float32Array(MAX_FRAMES); |
| 80 | let idx = 0; |
| 81 | const firstFrame = Date.now(); |
| 82 | function drawFrame() { |
| 83 | const now = performance.now(); |
| 84 | // Actually draw stuff. |
| 85 | const seek = ((Date.now() - firstFrame) / duration) % 1.0; |
| 86 | const damage = animation.seek(seek); |
| 87 | const afterSeek = performance.now(); |
| 88 | |
| 89 | if (damage.fRight > damage.fLeft && damage.fBottom > damage.fTop) { |
| 90 | canvas.clear(clearColor); |
| 91 | animation.render(canvas, bounds); |
| 92 | } |
| 93 | surface.flush(); |
| 94 | // FPS measurement |
| 95 | frames[idx] = performance.now() - now; |
| 96 | seeks[idx] = afterSeek - now; |
| 97 | idx++; |
| 98 | // If we have maxed out the frames we are measuring or have completed the animation, |
| 99 | // we stop benchmarking. |
| 100 | if (idx >= frames.length || (Date.now() - firstFrame) > MAX_SAMPLE_MS) { |
| 101 | window._perfData = { |
| 102 | frames_ms: Array.from(frames).slice(0, idx), |
| 103 | seeks_ms: Array.from(seeks).slice(0, idx), |
| 104 | json_load_ms: loadTime, |
| 105 | }; |
| 106 | window._perfDone = true; |
| 107 | return; |
| 108 | } |
| 109 | window.requestAnimationFrame(drawFrame); |
| 110 | } |
| 111 | window.requestAnimationFrame(drawFrame); |
| 112 | }); |
| 113 | console.log('Perf is ready'); |
| 114 | window._perfReady = true; |
| 115 | }); |
| 116 | })(); |
| 117 | |
| 118 | function getSurface(CanvasKit) { |
| 119 | let surface; |
| 120 | if (window.location.hash.indexOf('gpu') !== -1) { |
| 121 | surface = CanvasKit.MakeWebGLCanvasSurface('anim', WIDTH, HEIGHT); |
| 122 | if (!surface) { |
| 123 | window._error = 'Could not make GPU surface'; |
| 124 | return null; |
| 125 | } |
| 126 | let c = document.getElementById('anim'); |
| 127 | // If CanvasKit was unable to instantiate a WebGL context, it will fallback |
| 128 | // to CPU and add a ck-replaced class to the canvas element. |
| 129 | if (c.classList.contains('ck-replaced')) { |
| 130 | window._error = 'fell back to CPU'; |
| 131 | return null; |
| 132 | } |
| 133 | } else { |
| 134 | surface = CanvasKit.MakeSWCanvasSurface('anim', WIDTH, HEIGHT); |
| 135 | if (!surface) { |
| 136 | window._error = 'Could not make CPU surface'; |
| 137 | return null; |
| 138 | } |
| 139 | } |
| 140 | return surface; |
| 141 | } |
| 142 | |
| 143 | function loadFonts(fonts) { |
| 144 | const promises = []; |
| 145 | if (!fonts || !fonts.list) { |
| 146 | return promises; |
| 147 | } |
| 148 | for (const font of fonts.list) { |
| 149 | if (font.fName) { |
| 150 | promises.push(fetch(`${ASSETS_PATH}/${font.fName}.ttf`).then((resp) => { |
| 151 | // fetch does not reject on 404 |
| 152 | if (!resp.ok) { |
| 153 | console.error(`Could not load ${font.fName}.ttf: status ${resp.status}`); |
| 154 | return null; |
| 155 | } |
| 156 | return resp.arrayBuffer().then((buffer) => { |
| 157 | return { |
| 158 | 'name': font.fName, |
| 159 | 'bytes': buffer |
| 160 | }; |
| 161 | }); |
| 162 | }) |
| 163 | ); |
| 164 | } |
| 165 | } |
| 166 | return promises; |
| 167 | } |
| 168 | |
| 169 | function loadAssets(assets) { |
| 170 | const promises = []; |
| 171 | for (const asset of assets) { |
| 172 | // asset.p is the filename, if it's an image. |
| 173 | // Don't try to load inline/dataURI images. |
| 174 | const should_load = asset.p && asset.p.startsWith && !asset.p.startsWith('data:'); |
| 175 | if (should_load) { |
| 176 | promises.push(fetch(`${ASSETS_PATH}/${asset.p}`) |
| 177 | .then((resp) => { |
| 178 | // fetch does not reject on 404 |
| 179 | if (!resp.ok) { |
| 180 | console.error(`Could not load ${asset.p}: status ${resp.status}`); |
| 181 | return null; |
| 182 | } |
| 183 | return resp.arrayBuffer().then((buffer) => { |
| 184 | return { |
| 185 | 'name': asset.p, |
| 186 | 'bytes': buffer |
| 187 | }; |
| 188 | }); |
| 189 | }) |
| 190 | ); |
| 191 | } |
| 192 | } |
| 193 | return promises; |
| 194 | } |
| 195 | </script> |
| 196 | </body> |
| 197 | </html> |