├── .gitignore ├── LICENSE ├── README.md ├── config └── license-key ├── license-key ├── package-lock.json ├── package.json ├── public ├── assets │ └── default.pdf ├── index.html └── license-key └── src ├── index.js ├── lib ├── runner.js ├── tests.js └── utils.js └── ui ├── components ├── App.js ├── Benchmark.js ├── Footer.js ├── Introduction.js ├── PSPDFKit.js └── Test.js ├── index.css ├── logo.png └── render.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # artifacts 13 | /public/vendor 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The Nutrient Web SDK component is under a commercial license. 2 | Ping sales@nutrient.io for details. 3 | 4 | The remaining benchmark code is under MIT: 5 | 6 | The MIT License (MIT) 7 | 8 | Copyright (c) 2018-present PSPDFKit GmbH (www.nutrient.io) 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in 18 | all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | THE SOFTWARE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > ⚠️ **Repository Moved** 2 | > This repository has been moved to https://github.com/PSPDFKit/nutrient-web-examples/tree/main/examples/wasm-benchmark. 3 | > Please update your bookmarks and issues accordingly. 4 | > 5 | > This repo is now archived and will no longer receive updates. 6 | 7 |
8 | 9 | 10 | 11 |
12 | 13 | # WebAssembly Benchmark by Nutrient 14 | 15 | A Benchmark for WebAssembly (Wasm, WA) that uses [Nutrient Web](https://www.nutrient.io/sdk/web/) Standalone. 16 | 17 | The rendering engine of [Nutrient Web](https://www.nutrient.io/sdk/web/) Standalone is written in C/C++ and compiled to Wasm. 18 | 19 | Get your score in the [live demo](http://iswebassemblyfastyet.com/) and learn more in our [blog post](https://www.nutrient.io/blog/2018/a-real-world-webassembly-benchmark/). 20 | 21 | ## Prerequisites 22 | 23 | - [Node.js](http://nodejs.org/) (with npm or Yarn) 24 | - A Nutrient Web license. If you don't already have one 25 | you can [request a free trial here](https://www.nutrient.io/try/). 26 | 27 | ## Getting Started 28 | 29 | Install the `nutrient` npm package and move all contents to the vendor directory. 30 | 31 | ```bash 32 | npm install --save @nutrient-sdk/viewer 33 | mkdir -p public/vendor 34 | cp -R node_modules/nutrient/dist public/vendor/nutrient 35 | ``` 36 | 37 | Bootstrap the project by installing all the other dependencies. 38 | 39 | ```bash 40 | npm install 41 | ``` 42 | 43 | ## Running the Benchmark 44 | 45 | Now that Nutrient Web is installed, you need to copy your product (license) key to the `public/license-key` file. 46 | 47 | We can now run the benchmark server: 48 | 49 | ```bash 50 | npm start 51 | ``` 52 | 53 | The benchmark is available at `http://localhost:3000`. 54 | 55 | ## Building a Production Version 56 | 57 | You can build an optimized version using the following command: 58 | 59 | ```bash 60 | PUBLIC_URL="/webassembly-benchmark/" npm run build 61 | ``` 62 | 63 | Where `PUBLIC_URL` must be set according to the final URL, where the application is hosted. 64 | 65 | ## Optimizations 66 | 67 | The following optimizations can be enabled via URL parameter: 68 | 69 | - `disableWebAssemblyStreaming`, `true` by default 70 | - `standaloneInstancesPoolSize`, `0` by default 71 | - `runsScaleFactor`, scales the number of test runs, `1` by default 72 | 73 | ## What's in This Repository 74 | 75 | This repository contains files used to build the [Nutrient WebAssembly benchmark](http://iswebassemblyfastyet.com/). 76 | 77 | The source files are structured into two different categories: 78 | 79 | - `src/lib` contains all files necessary to set up the test suite including the individual tests and helper functions. 80 | - `src/ui` contains a [React](https://reactjs.org/) application that is used to render the user interface. 81 | 82 | For a main entry point, have a look at `src/index.js`. 83 | 84 | ## License 85 | 86 | This software is licensed under [the MIT license](LICENSE). 87 | 88 | ## Contributing 89 | 90 | Please ensure 91 | [you have signed our CLA](https://www.nutrient.io/guides/web/current/miscellaneous/contributing/) so that we can 92 | accept your contributions. 93 | -------------------------------------------------------------------------------- /config/license-key: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /license-key: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nutrient-webassembly-benchmark", 3 | "version": "1.0.0", 4 | "private": true, 5 | "engines": { 6 | "npm": ">=8.3.0" 7 | }, 8 | "dependencies": { 9 | "details-polyfill": "^1.1.0", 10 | "react": "^16.4.1", 11 | "react-dom": "^16.4.1", 12 | "react-scripts": "^5.0.1" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test --env=jsdom", 18 | "eject": "react-scripts eject" 19 | }, 20 | "browserslist": [ 21 | "Firefox ESR", 22 | "last 2 Chrome versions", 23 | "last 2 firefox versions", 24 | "Edge 18", 25 | "last 2 safari versions", 26 | "last 2 and_chr versions", 27 | "last 2 ios_saf versions", 28 | "safari >= 15.4", 29 | "ios_saf >= 15.4" 30 | ], 31 | "overrides": { 32 | "colors": "^1.4.0", 33 | "minimist": "^1.2.6", 34 | "nth-check": "^2.0.1", 35 | "url-parse": "^1.5.9", 36 | "handlebars": "^4.7.7", 37 | "async@>= 3.0.0 < 3.2.2": "^3.2.2", 38 | "async@< 2.6.4": "^2.6.4", 39 | "loader-utils": "^2.0.4", 40 | "json5": "^2.2.2", 41 | "postcss": "^8.4.31", 42 | "rollup": "^3.29.5", 43 | "cross-spawn": "^7.0.5", 44 | "path-to-regexp": "^0.1.12", 45 | "cookie": "^0.7.0", 46 | "http-proxy-middleware": "^2.0.7" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /public/assets/default.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PSPDFKit-labs/pspdfkit-webassembly-benchmark/145c697cf95e140fbc6ccd7ddc18916649096066/public/assets/default.pdf -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebAssembly Benchmark by Nutrient 6 | 10 | 11 | 12 | 13 | 14 |
15 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /public/license-key: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import "details-polyfill"; 2 | 3 | import render from "./ui/render"; 4 | import { getConfigOptionsFromURL } from "./lib/utils"; 5 | import { createBenchmark } from "./lib/tests"; 6 | 7 | // PDF to benchmark against. 8 | const PDF = "./assets/default.pdf"; 9 | 10 | let state = { 11 | tests: { 12 | // We set the first test to running so we avoid a state where all is idle. 13 | "Test-Initialization": { state: "running", progress: 0 }, 14 | }, 15 | isWasm: true, 16 | error: null, 17 | state: "running", 18 | pspdfkitScore: 0, 19 | loadTimeInPspdfkitScore: 0, 20 | document: null, 21 | licenseKey: null, 22 | }; 23 | 24 | render(state); 25 | 26 | (async function () { 27 | try { 28 | // Load the PDF and the license key. 29 | const [pdf, licenseKey] = await Promise.all([ 30 | await fetch(PDF).then((r) => r.arrayBuffer()), 31 | await fetch("./license-key").then((response) => response.text()), 32 | ]); 33 | 34 | const { pspdfkitConfig } = getConfigOptionsFromURL(); 35 | 36 | const benchmark = createBenchmark(pdf, licenseKey, pspdfkitConfig); 37 | 38 | state.pdf = pdf; 39 | state.licenseKey = licenseKey; 40 | state.isWasm = benchmark.isWasm; 41 | render(state); 42 | 43 | // We pre-fetch some assets in order to not affect the benchmark results. 44 | const preFetchAssets = [ 45 | state.isWasm && "./vendor/nutrient/nutrient-viewer-lib/pspdfkit.wasm.js", 46 | state.isWasm && "./vendor/nutrient/nutrient-viewer-lib/pspdfkit.wasm", 47 | ] 48 | .filter(Boolean) 49 | .map((asset) => fetch(asset)); 50 | 51 | await Promise.all(preFetchAssets); 52 | 53 | const score = await benchmark.run((updatedTests) => { 54 | state.tests = updatedTests; 55 | render(state); 56 | }); 57 | 58 | state.state = "done"; 59 | state.pspdfkitScore = Math.round(score.load + score.rest); 60 | state.loadTimeInPspdfkitScore = Math.round( 61 | (100 * score.load) / state.pspdfkitScore 62 | ); 63 | render(state); 64 | 65 | if (window.ga) { 66 | window.ga( 67 | "send", 68 | "event", 69 | "wasmbench", 70 | "score", 71 | state.isWasm ? "wasm-score" : "asmjs-score", 72 | state.pspdfkitScore 73 | ); 74 | window.ga( 75 | "send", 76 | "event", 77 | "wasmbench", 78 | "ratio", 79 | state.isWasm ? "wasm-ratio" : "asmjs-ratio", 80 | state.loadTimeInPspdfkitScore 81 | ); 82 | } 83 | } catch (e) { 84 | console.error(e); 85 | state.error = e; 86 | render(state); 87 | } 88 | })(); 89 | -------------------------------------------------------------------------------- /src/lib/runner.js: -------------------------------------------------------------------------------- 1 | import { 2 | cleanupMeasurement, 3 | clearAllTimings, 4 | getConfigOptionsFromURL, 5 | median, 6 | } from "./utils"; 7 | 8 | export function createRunner(licenseKey) { 9 | let tests = {}; 10 | 11 | // Register a benchmark to test 12 | function bench(id, benchmarkFn, opts) { 13 | if (typeof opts === "undefined") { 14 | opts = {}; 15 | } 16 | 17 | tests[id] = { 18 | benchmarkFn, 19 | opts, 20 | state: "idle", 21 | progress: 0, 22 | totalTime: 0, 23 | medians: [], 24 | }; 25 | } 26 | 27 | // Run the test suite. The `onChange` callback will fire whenever the progress 28 | // of a single test is changed. 29 | async function run(onChange) { 30 | function notify() { 31 | onChange(tests); 32 | } 33 | 34 | let score = { 35 | load: 0, 36 | rest: 0, 37 | }; 38 | 39 | for (let testId in tests) { 40 | let test = tests[testId]; 41 | 42 | const { opts, benchmarkFn } = test; 43 | const totalRuns = opts.times || 1; 44 | 45 | // Mark the current test as running and re-render. 46 | test.state = "running"; 47 | 48 | notify(); 49 | 50 | let measurements = {}; 51 | 52 | for (let i = 0; i < totalRuns; i++) { 53 | clearAllTimings(); 54 | 55 | const results = await benchmarkFn(); 56 | 57 | // benchmarkFn returns an array of performance measurement objects. In 58 | // a first step, we filter those results. 59 | results.forEach((result) => { 60 | const { name } = result; 61 | 62 | if (!measurements[name]) { 63 | measurements[name] = []; 64 | } 65 | 66 | measurements[name].push( 67 | // remove time to load chunks and the pdf from `load` 68 | opts.bucket === "load" 69 | ? { 70 | name: result.name, 71 | duration: cleanupMeasurement(result.duration), 72 | } 73 | : { name: result.name, duration: result.duration } 74 | ); 75 | }); 76 | 77 | // Update the progress indicator and re-render. 78 | test.progress = Math.round((100 * (i + 1)) / totalRuns); 79 | 80 | notify(); 81 | } 82 | 83 | // Collect all relevant timings. 84 | const medians = Object.keys(measurements).map((name) => ({ 85 | name, 86 | median: median(measurements[name].map((m) => m.duration)), 87 | })); 88 | const totalTime = Math.round( 89 | medians.reduce((sum, { median }) => sum + median, 0) 90 | ); 91 | 92 | // Add the total time of the test to the final score. 93 | if (score.hasOwnProperty(opts.bucket)) { 94 | if (opts.bucket !== "load" || score.load === 0) { 95 | score[opts.bucket] += totalTime; 96 | } 97 | } 98 | 99 | // Update the state and re-render 100 | test.totalTime = totalTime; 101 | 102 | test.medians = medians; 103 | 104 | test.state = "complete"; 105 | 106 | notify(); 107 | } 108 | 109 | return score; 110 | } 111 | 112 | function load(pdf, conf = {}) { 113 | const defaultConf = getConfigOptionsFromURL().pspdfkitConfig; 114 | const configuration = Object.assign( 115 | { 116 | document: pdf, 117 | headless: true, 118 | licenseKey, 119 | }, 120 | defaultConf, 121 | conf 122 | ); 123 | 124 | return window.PSPDFKit.load(configuration).then(function (instance) { 125 | return { 126 | instance, 127 | unload: function () { 128 | window.PSPDFKit.unload(instance); 129 | instance = null; 130 | pdf = null; 131 | }, 132 | }; 133 | }); 134 | } 135 | 136 | return { load, bench, run }; 137 | } 138 | -------------------------------------------------------------------------------- /src/lib/tests.js: -------------------------------------------------------------------------------- 1 | import { clearAllTimings, isMobileOS, isWasmSupported } from "./utils"; 2 | import { createRunner } from "./runner"; 3 | 4 | export function createBenchmark(pdf, licenseKey, conf) { 5 | // Factory to create our test suite. It will register all tests in the runner. 6 | const isWasm = isWasmSupported() && !conf.disableWebAssembly; 7 | 8 | const runner = createRunner(licenseKey); 9 | 10 | runner.bench( 11 | "Test-Rendering", 12 | async function () { 13 | const instance = await prepareInstance(); 14 | const totalPageCount = instance.totalPageCount; 15 | 16 | let promises = []; 17 | 18 | for (let pageIndex = 0; pageIndex < totalPageCount; pageIndex++) { 19 | promises.push( 20 | instance.renderPageAsArrayBuffer({ height: 1024 }, pageIndex) 21 | ); 22 | } 23 | 24 | performance.mark("renderStart"); 25 | await Promise.all(promises); 26 | performance.mark("renderEnd"); 27 | 28 | performance.measure( 29 | "render tiles for a page", 30 | "renderStart", 31 | "renderEnd" 32 | ); 33 | 34 | return performance.getEntriesByType("measure"); 35 | }, 36 | { 37 | times: scaleRuns(10), 38 | bucket: "rest", 39 | } 40 | ); 41 | 42 | runner.bench( 43 | "Test-Searching", 44 | async function () { 45 | const instance = await prepareInstance(); 46 | 47 | let promises = []; 48 | 49 | for (let i = 0; i < 50; i++) { 50 | promises.push(instance.search("the")); 51 | } 52 | 53 | performance.mark("searchStart"); 54 | await Promise.all(promises); 55 | performance.mark("searchEnd"); 56 | 57 | performance.measure("search text", "searchStart", "searchEnd"); 58 | 59 | return performance.getEntriesByType("measure"); 60 | }, 61 | { 62 | times: scaleRuns(10), 63 | bucket: "rest", 64 | } 65 | ); 66 | 67 | runner.bench( 68 | "Test-Exporting", 69 | async function () { 70 | const instance = await prepareInstance(); 71 | 72 | performance.mark("exportStart"); 73 | await Promise.all([ 74 | instance.exportPDF(), 75 | instance.exportPDF(), 76 | instance.exportPDF({ flatten: true }), 77 | instance.exportPDF({ flatten: true }), 78 | instance.exportXFDF(), 79 | instance.exportXFDF(), 80 | ]); 81 | performance.mark("exportEnd"); 82 | 83 | performance.measure("different exports", "exportStart", "exportEnd"); 84 | 85 | return performance.getEntriesByType("measure"); 86 | }, 87 | { 88 | times: scaleRuns(10), 89 | bucket: "rest", 90 | } 91 | ); 92 | 93 | runner.bench( 94 | "Test-Annotations", 95 | async function () { 96 | const instance = await prepareInstance(); 97 | 98 | const annotation = new window.PSPDFKit.Annotations.TextAnnotation({ 99 | pageIndex: 0, 100 | text: { format: "plain", value: "test" }, 101 | boundingBox: new window.PSPDFKit.Geometry.Rect({ 102 | width: 200, 103 | height: 30, 104 | }), 105 | }); 106 | const range = [...new Array(100)]; 107 | 108 | performance.mark("createAnnotationStart"); 109 | 110 | const annotations = ( 111 | await Promise.all(range.map(() => instance.create(annotation))) 112 | ).map((createdAnnotations) => createdAnnotations[0]); 113 | 114 | // Nutrient Web SDK will only write annotations back to the PDF when it 115 | // has to. To make sure this is happening, we profile the exportPDF 116 | // endpoint. 117 | await instance.exportPDF(); 118 | 119 | await Promise.all(annotations.map((a) => instance.delete(a.id))); 120 | 121 | performance.mark("createAnnotationEnd"); 122 | 123 | performance.measure( 124 | "create annotation", 125 | "createAnnotationStart", 126 | "createAnnotationEnd" 127 | ); 128 | 129 | return performance.getEntriesByType("measure"); 130 | }, 131 | { 132 | times: scaleRuns(10), 133 | bucket: "rest", 134 | } 135 | ); 136 | 137 | runner.bench( 138 | "Test-Initialization", 139 | async function () { 140 | performance.mark("loadStart"); 141 | 142 | await prepareInstance( 143 | /* canReuseLastOne */ false, 144 | /* clearTimings */ false 145 | ); 146 | 147 | performance.mark("loadEnd"); 148 | performance.measure("load", "loadStart", "loadEnd"); 149 | 150 | return performance.getEntriesByType("measure"); 151 | }, 152 | { 153 | // We decrease the number of initialization runs on mobile systems to avoid OOM errors. 154 | times: scaleRuns(isMobileOS() ? 2 : 3), 155 | bucket: "load", 156 | } 157 | ); 158 | 159 | // We want to reuse the instance in the following tests. To achieve this, we 160 | // store it in the function closure. 161 | let instance, unload; 162 | 163 | async function prepareInstance(canReuseLastOne = true, clearTimings = true) { 164 | if (!canReuseLastOne) { 165 | unload && unload(); 166 | unload = null; 167 | instance = null; 168 | } 169 | 170 | if (!instance) { 171 | const result = await runner.load(pdf.slice(0), conf); 172 | 173 | instance = result.instance; 174 | unload = result.unload; 175 | clearTimings && clearAllTimings(); 176 | } 177 | 178 | return instance; 179 | } 180 | 181 | // It's possible to scale the individual test runs by a scale factor defined 182 | // as a query parameter `?runsScaleFactor=2` would run all test twice as much as 183 | // regular. 184 | // 185 | // This will always return at least one. 186 | function scaleRuns(runs) { 187 | const params = {}; 188 | 189 | window.location.search 190 | .substring(1) 191 | .replace(/([^=&]+)=([^&]*)/g, (m, key, value) => { 192 | params[decodeURIComponent(key)] = decodeURIComponent(value); 193 | }); 194 | 195 | let runsScaleFactor = 1; 196 | 197 | if (typeof params.runsScaleFactor !== "undefined") { 198 | runsScaleFactor = parseFloat(params.runsScaleFactor); 199 | 200 | if (isNaN(runsScaleFactor)) { 201 | runsScaleFactor = 1; 202 | } 203 | } 204 | 205 | runs = Math.ceil(runs * runsScaleFactor); 206 | 207 | return Math.min( 208 | // We always want to return at least one run. 209 | Math.max(1, runs), 210 | // Anything higher then 100 runs does not really make any sense 211 | 100 212 | ); 213 | } 214 | 215 | return { 216 | run: runner.run, 217 | isWasm: isWasm, 218 | }; 219 | } 220 | -------------------------------------------------------------------------------- /src/lib/utils.js: -------------------------------------------------------------------------------- 1 | // This function takes a `duration` and subtracts the time to load chunks 2 | // (Wasm artifacts) which otherwise would influence the final benchmark 3 | // result. 4 | const ignoredResourceRegex = /.*nutrient.w?asm.*/; 5 | 6 | export function cleanupMeasurement(duration) { 7 | const ignoredResources = performance 8 | .getEntriesByType("resource") 9 | .filter((r) => ignoredResourceRegex.test(r.name)); 10 | 11 | const noise = ignoredResources.reduce((time, resource) => { 12 | time += resource.duration; 13 | 14 | return time; 15 | }, 0); 16 | 17 | if (noise >= duration) { 18 | console.warn( 19 | "An error occurred while calculating the network noise. Including network noise in this example.", 20 | { 21 | duration, 22 | noise, 23 | ignoredResources, 24 | } 25 | ); 26 | 27 | return duration; 28 | } 29 | 30 | return duration - noise; 31 | } 32 | 33 | export function clearAllTimings() { 34 | performance.clearMarks(); 35 | performance.clearMeasures(); 36 | performance.clearResourceTimings(); 37 | } 38 | 39 | // Given an array of numbers it calculates the median value. 40 | export function median(arr) { 41 | arr = arr.slice(0); 42 | 43 | arr.sort((a, b) => a - b); 44 | 45 | const half = Math.floor(arr.length / 2); 46 | 47 | if (arr.length % 2) { 48 | return arr[half]; 49 | } else { 50 | return (arr[half - 1] + arr[half]) / 2.0; 51 | } 52 | } 53 | 54 | // Parses the url to retrieve the configuration options for Nutrient Web. 55 | export function getConfigOptionsFromURL() { 56 | const params = {}; 57 | 58 | window.location.search 59 | .substring(1) 60 | .replace(/([^=&]+)=([^&]*)/g, (m, key, value) => { 61 | params[decodeURIComponent(key)] = decodeURIComponent(value); 62 | }); 63 | 64 | const standaloneInstancesPoolSize = parseInt( 65 | params.standaloneInstancesPoolSize, 66 | 10 67 | ); 68 | 69 | return { 70 | pspdfkitConfig: { 71 | disableWebAssembly: params.disableWebAssembly === "true", 72 | disableWebAssemblyStreaming: 73 | params.disableWebAssemblyStreaming === "false" ? false : true, 74 | standaloneInstancesPoolSize: isNaN(standaloneInstancesPoolSize) 75 | ? 0 76 | : standaloneInstancesPoolSize, 77 | }, 78 | writeResults: params.writeResults === "true", 79 | }; 80 | } 81 | 82 | // The same Wasm test that is used in Nutrient Web 83 | export function isWasmSupported() { 84 | try { 85 | // iOS ~11.2.2 has a known Wasm problem. 86 | // See: https://github.com/kripken/emscripten/issues/6042 87 | if ( 88 | /iPad|iPhone|iPod/.test(navigator.userAgent) && 89 | /11_2_\d+/.test(navigator.userAgent) 90 | ) { 91 | return false; 92 | } 93 | } catch (_) { 94 | // In case of an error, we simply continue to the regular feature check 95 | } 96 | 97 | return ( 98 | typeof window.WebAssembly === "object" && 99 | typeof window.WebAssembly.instantiate === "function" 100 | ); 101 | } 102 | 103 | // We don't want to show the final Nutrient Web view if we're on a mobile 104 | // browser and only have limited resources available. 105 | export function isMobileOS() { 106 | const { userAgent } = navigator; 107 | 108 | if (/windows phone/i.test(userAgent)) { 109 | return true; 110 | } 111 | 112 | if (/android/i.test(userAgent)) { 113 | return true; 114 | } 115 | 116 | if (/iPad|iPhone|iPod/.test(userAgent)) { 117 | return true; 118 | } 119 | 120 | return false; 121 | } 122 | -------------------------------------------------------------------------------- /src/ui/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | import Introduction from "./Introduction"; 4 | import Benchmark from "./Benchmark"; 5 | 6 | class App extends Component { 7 | render() { 8 | const { 9 | isWasm, 10 | error, 11 | state, 12 | tests, 13 | pspdfkitScore, 14 | loadTimeInPspdfkitScore, 15 | pdf, 16 | licenseKey, 17 | } = this.props; 18 | 19 | return ( 20 |
21 | 22 | {error ? ( 23 |
24 | An error occurred: {error.message}. Please{" "} 25 | 26 | reload 27 | 28 | . 29 |
30 | ) : ( 31 | 40 | )} 41 |
42 | ); 43 | } 44 | } 45 | 46 | export default App; 47 | -------------------------------------------------------------------------------- /src/ui/components/Benchmark.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Test from "./Test"; 4 | import Footer from "./Footer"; 5 | import PSPDFKit from "./PSPDFKit"; 6 | import { isMobileOS } from "../../lib/utils"; 7 | 8 | export default class Benchmark extends React.Component { 9 | render() { 10 | const { 11 | isWasm, 12 | state, 13 | tests, 14 | pspdfkitScore, 15 | loadTimeInPspdfkitScore, 16 | pdf, 17 | licenseKey, 18 | } = this.props; 19 | 20 | return ( 21 | 22 | 26 | 27 | 33 |

34 | In this benchmark, we measure the rendering time for all pages 35 | using the{" "} 36 | 41 | Instance#renderPageAsArrayBuffer API 42 | 43 | . 44 |

45 |

46 | In production, we use multiple techniques to replace part of the 47 | rendered PDF pages as you zoom in to deliver a sharp document at 48 | any zoom level and resolution. 49 |

50 |
51 | } 52 | /> 53 | 54 | 60 | In this benchmark, we use our{" "} 61 | 66 | search API 67 | {" "} 68 | to search some text programmatically. 69 |

70 | } 71 | /> 72 | 73 | 79 |

80 | In this benchmark, we export a PDF document to file, to 81 | InstantJSON, and to XFDF. 82 |

83 |

84 | Nutrient developed a JSON spec for PDF annotations and form 85 | fields called{" "} 86 | 91 | InstantJSON 92 | 93 | . This is a modern and clean JSON spec that all Nutrient 94 | products use to import and export metadata. 95 |

96 |

97 | Nutrient also comes with full{" "} 98 | 103 | XFDF support 104 | 105 | . 106 |

107 | 108 | } 109 | /> 110 | 111 | 117 | In this benchmark, we use our{" "} 118 | 123 | annotations API 124 | {" "} 125 | to programmatically create 100 annotations and then export them as 126 | a PDF. 127 |

128 | } 129 | /> 130 | 131 | 141 |

142 | The initialization process consists of three steps: downloading, 143 | compiling, and instantiation. In this benchmark, we ignore 144 | download time. 145 |

146 |

147 | Read about how you can{" "} 148 | 153 | optimize startup time 154 | 155 | . 156 |

157 |

158 | This test must run at the end to avoid penalizing browsers that 159 | run a baseline compiler first. In those cases, multiple 160 | initializations might start a background compilation which slows 161 | down tests that run afterwards. 162 |

163 | 164 | } 165 | /> 166 | 167 |
168 |
169 |
170 | {state === "running" ? ( 171 | 172 | Running 173 | 174 | ... 175 | 176 | 177 | ) : ( 178 | "All done!" 179 | )} 180 |
181 | {state === "done" && ( 182 |
183 |
184 | {isWasm && ( 185 |
Nutrient Wasm Score
186 | )} 187 | {!isWasm && ( 188 |
Nutrient JavaScript Score
189 | )} 190 |
{pspdfkitScore}
191 |
192 |
193 | )} 194 |
195 | 196 | {state === "done" && ( 197 |
198 |

199 | The score is the total benchmark time. The lower the score, the 200 | better. 201 |

202 |

203 | In this benchmark, we defined two buckets to measure{" "} 204 | compilation/instantiation time and computation{" "} 205 | time. This enables us to perform a fairer comparison between 206 | vendors, since loading time can make a difference. 207 |

208 |
209 |
213 |
214 |

215 | For this browser, compilation/instantiation time accounts for{" "} 216 | {loadTimeInPspdfkitScore}% of the total time. 217 |

218 |
219 | )} 220 |
221 | 222 | {state === "done" && ( 223 | 228 | )} 229 | 230 | {state === "done" && !isMobileOS() && ( 231 | 232 | )} 233 | 234 |