├── .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 | You need to enable JavaScript to run this app.
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 |
23 |
Type of benchmark
24 |
Median duration
25 |
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 |
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 |
235 |
236 | );
237 | }
238 | }
239 |
--------------------------------------------------------------------------------
/src/ui/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default function Footer() {
4 | return (
5 |
6 | Version 2.0, Nutrient Web {window.PSPDFKit.version}.
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/src/ui/components/Introduction.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import logo from "../logo.png";
4 |
5 | export default function Introduction({ isWasm }) {
6 | return (
7 |
8 |
9 |
14 |
15 |
16 |
17 | Welcome to the WebAssembly Benchmark by Nutrient, a real-world
18 | benchmark based on{" "}
19 | Nutrient Web . Want to
20 | know more about the benchmark? Read the{" "}
21 |
22 | announcement blog post
23 |
24 | .
25 |
26 |
27 |
28 |
29 | {isWasm && (
30 |
31 | You’re running the WebAssembly Benchmark! For browsers that don’t
32 | support Wasm, we made a benchmark that runs a JavaScript version of
33 | Nutrient Web.
34 | {isWasm && (
35 |
36 | {" "}
37 | You can find it here .
38 |
39 | )}
40 |
41 | )}
42 | {!isWasm && (
43 |
44 | You’re running our benchmark using a compiled-to-JavaScript version
45 | of our PDF engine instead of the WebAssembly one. You can find the
46 | original benchmark here .
47 |
48 | )}
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/ui/components/PSPDFKit.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default class PSPDFKit extends React.Component {
4 | ref = React.createRef();
5 |
6 | async componentDidMount() {
7 | const { pdf, licenseKey, isWasm } = this.props;
8 |
9 | window.PSPDFKit.load({
10 | document: pdf,
11 | licenseKey,
12 | container: this.ref.current,
13 | disableWebAssemblyStreaming: true,
14 | disableWebAssembly: !isWasm,
15 | standaloneInstancesPoolSize: 1,
16 | });
17 | }
18 |
19 | render() {
20 | return (
21 |
24 | );
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/ui/components/Test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default class Test extends React.Component {
4 | render() {
5 | const { id, heading, description, data } = this.props;
6 |
7 | const state = data ? data.state : "idle";
8 | const progress = data ? data.progress : 0;
9 | const totalTime = data ? data.totalTime : 0;
10 | const medians = data ? data.medians : [];
11 |
12 | const summary = (
13 |
14 |
15 |
{heading}
16 | {state === "idle" && Pending... }
17 | {state === "running" && (
18 |
19 | Running
20 |
21 | ...
22 |
23 |
24 | )}
25 |
26 | {state === "complete" && (
27 |
28 | {Math.round(totalTime)} ms
29 |
30 | )}
31 |
32 |
46 |
47 | );
48 |
49 | const details = (
50 |
51 | {description}
52 |
53 | {medians && medians.length > 1 && state === "complete" && (
54 |
55 |
Partial results
56 | {medians.map(({ name, median }) => (
57 |
58 | {name}
59 | {Math.round(median)} ms
60 |
61 | ))}
62 |
63 | )}
64 |
65 | );
66 |
67 | return (
68 |
69 | {state === "running" ? (
70 |
71 | {summary}
72 | {details}
73 |
74 | ) : (
75 |
76 | {summary}
77 | {details}
78 |
79 | )}
80 |
81 | );
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/ui/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | html {
6 | font-size: 16px;
7 | }
8 | body {
9 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
10 | Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
11 | font-size: 1em;
12 | line-height: 1.375rem;
13 | margin: 0;
14 | padding: 0 2rem;
15 | color: #434549;
16 | background-color: #eceded;
17 | font-weight: 300;
18 | }
19 |
20 | @media (max-width: 1085px) {
21 | html {
22 | font-size: 18px;
23 | }
24 | body {
25 | padding: 0 3.25rem;
26 | }
27 | }
28 |
29 | a {
30 | color: #06478e;
31 | font-weight: 600;
32 | text-decoration: none;
33 | }
34 |
35 | a:hover,
36 | a:focus {
37 | text-decoration: underline;
38 | }
39 |
40 | .App {
41 | max-width: 795px;
42 | width: 100%;
43 | margin: 5.625rem auto;
44 | counter-reset: bench-number;
45 | }
46 |
47 | .Logo {
48 | width: 40%;
49 | margin: 0 auto;
50 | display: block;
51 | }
52 |
53 | .Description {
54 | margin: 5.625rem 0 3.75rem 0;
55 | font-size: 1.25rem;
56 | line-height: 2.0625rem;
57 | }
58 |
59 | .Switch {
60 | padding: 1.25rem;
61 | border-radius: 0.25rem;
62 | background-color: #fff;
63 | }
64 |
65 | .Switch p {
66 | margin: 0;
67 | line-height: 1.8125rem;
68 | }
69 |
70 | .BenchsHeader {
71 | display: flex;
72 | justify-content: space-between;
73 | margin-top: 7.5rem;
74 | font-size: 1rem;
75 | line-height: 1.8125rem;
76 | font-weight: 600;
77 | color: #9ea0a5;
78 | border-bottom: 0.125rem solid #9ea0a5;
79 | }
80 |
81 | .BenchsHeader + .Bench {
82 | margin-top: 3.75rem;
83 | }
84 |
85 | .Bench {
86 | margin-top: 7.5rem;
87 | position: relative;
88 | }
89 |
90 | .Bench:before {
91 | counter-increment: bench-number;
92 | content: counter(bench-number);
93 | position: absolute;
94 | width: 1.6875rem;
95 | height: 1.6875rem;
96 | line-height: 1.6875rem;
97 | top: 0;
98 | left: calc(-1em + -2.1875rem);
99 | text-align: center;
100 | border-radius: 100%;
101 | background-color: #fff;
102 | font-size: 0.875rem;
103 | font-weight: bold;
104 | }
105 |
106 | .Bench--running {
107 | margin-left: 0;
108 | }
109 |
110 | .Bench--running:before {
111 | background-color: #3b81da;
112 | color: #fff;
113 | }
114 |
115 | .Bench--complete:before {
116 | background-color: #9ea0a5;
117 | color: #fff;
118 | }
119 |
120 | .Bench-heading {
121 | display: flex;
122 | justify-content: space-between;
123 | align-items: center;
124 | font-size: 1.25rem;
125 | line-height: 1.6875rem;
126 | font-weight: 600;
127 | margin-top: -1.25em;
128 | margin-left: 1em;
129 | padding-bottom: 0.3125rem;
130 | }
131 |
132 | .Bench-heading h3 {
133 | font-size: inherit;
134 | line-height: inherit;
135 | font-weight: 1;
136 | margin: 0;
137 | }
138 |
139 | .Bench-info {
140 | display: flex;
141 | white-space: nowrap;
142 | color: #9ea0a5;
143 | }
144 |
145 | .Bench--running .Bench-info {
146 | color: #3b81da;
147 | }
148 |
149 | .Bench--running .Bench-heading {
150 | margin-top: 0;
151 | margin-left: 0;
152 | }
153 |
154 | .Bench--complete .Bench-info {
155 | color: #434549;
156 | }
157 |
158 | .Bench-description,
159 | .Bench-partials {
160 | line-height: 1.375rem;
161 | width: 90%;
162 | margin: 0 auto;
163 | }
164 |
165 | .Bench-description {
166 | margin-top: 1.375rem;
167 | line-height: 1.8125rem;
168 | }
169 |
170 | .Bench-description p {
171 | margin: 0;
172 | margin-top: 1.375rem;
173 | }
174 |
175 | .Bench-partialResult {
176 | display: flex;
177 | justify-content: space-between;
178 | align-items: center;
179 | border-bottom: 0.125rem solid;
180 | }
181 |
182 | .ProgressBar {
183 | background-color: #fff;
184 | }
185 |
186 | .ProgressBar-percentage {
187 | height: 0.25rem;
188 | background-color: #9ea0a5;
189 | transition: max-width 2s ease-out;
190 | }
191 |
192 | .Bench--running .ProgressBar-percentage {
193 | background-color: #3b81da;
194 | }
195 |
196 | .Progress {
197 | width: 1.25rem;
198 | text-align: left;
199 | }
200 |
201 | .Progress-dots {
202 | display: inline-block;
203 | vertical-align: bottom;
204 | overflow: hidden;
205 | animation: progress 0.65s linear infinite;
206 | }
207 |
208 | @keyframes progress {
209 | 0% {
210 | width: 0;
211 | }
212 | 100% {
213 | width: 1.25rem;
214 | }
215 | }
216 |
217 | .Result {
218 | margin: 3.75rem 0;
219 | display: flex;
220 | align-items: center;
221 | justify-content: space-between;
222 | background-color: #fff;
223 | color: #3b81da;
224 | font-size: 1.25rem;
225 | line-height: 1.6875rem;
226 | font-weight: 600;
227 | }
228 |
229 | .Result-message,
230 | .Score-label,
231 | .Score-value {
232 | padding: 0.625rem 1.25rem;
233 | }
234 |
235 | .Score {
236 | display: flex;
237 | }
238 |
239 | .Score-label {
240 | color: #434549;
241 | }
242 |
243 | .Score-value {
244 | background-color: #3b81da;
245 | color: #fff;
246 | text-align: center;
247 | }
248 |
249 | .ResultDetails {
250 | margin-top: 5.625rem;
251 | }
252 |
253 | .ResultDetails p {
254 | margin: 0;
255 | line-height: 1.8125rem;
256 | }
257 |
258 | .ResultDetails p + p {
259 | margin-top: 1.375rem;
260 | }
261 |
262 | .LoadTime {
263 | width: 100%;
264 | background-color: #fff;
265 | margin-top: 3.75rem;
266 | margin-bottom: 1.25rem;
267 | }
268 |
269 | .LoadTime-bar {
270 | height: 1rem;
271 | background-color: #f34579;
272 | }
273 |
274 | .Error {
275 | padding: 1rem;
276 | margin: 1rem 0;
277 | background-color: #feeeee;
278 | }
279 |
280 | .PSPDFKit-container {
281 | position: relative;
282 | width: 100%;
283 | padding-bottom: 100%;
284 | margin-top: 3.75rem;
285 | background-color: #fff;
286 | }
287 |
288 | #PSPDFKit-container {
289 | position: absolute;
290 | width: 100%;
291 | height: 100%;
292 | }
293 |
294 | .ResultCTA {
295 | text-align: center;
296 | margin: 3rem 0;
297 | }
298 |
299 | .Button {
300 | background: #3b81da;
301 | color: white;
302 | display: inline-block;
303 | padding: 0.5rem 1rem;
304 | }
305 |
306 | .Footer {
307 | text-align: center;
308 | padding: 1rem;
309 | background-color: #3b81da;
310 | color: whitesmoke;
311 | }
312 |
--------------------------------------------------------------------------------
/src/ui/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PSPDFKit-labs/pspdfkit-webassembly-benchmark/145c697cf95e140fbc6ccd7ddc18916649096066/src/ui/logo.png
--------------------------------------------------------------------------------
/src/ui/render.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 |
4 | import "./index.css";
5 | import App from "./components/App";
6 |
7 | export default function render({
8 | isWasm,
9 | tests,
10 | state,
11 | error,
12 | pspdfkitScore,
13 | loadTimeInPspdfkitScore,
14 | pdf,
15 | licenseKey,
16 | }) {
17 | ReactDOM.render(
18 | ,
28 | document.getElementById("root")
29 | );
30 | }
31 |
--------------------------------------------------------------------------------