Skip to content

Commit

Permalink
feat: add web-based benchmark page
Browse files Browse the repository at this point in the history
- Compare with PSD.js (fetched from CDN) and ag-psd (bundled)
- Checkbox to apply layer opacity when decoding (skipped by default)
  (it's supported by @webtoon/psd only and is very slow)
- Show stacked bar chart using Chart.js
- Show spinner while running benchmark
  (improves UX since progressbar is generally static)

Run `npm run start:benchmark` to launch the local development server,
and `npm run build:benchmark` to build (for publishing to GitHub pages).

Also, running `npm run deploy` will build both the browser demo and the
benchmark pages and deploy them to GitHub pages.
  • Loading branch information
pastelmind committed Jan 27, 2022
1 parent d16c6ab commit 9bc26f9
Show file tree
Hide file tree
Showing 14 changed files with 1,716 additions and 1 deletion.
Binary file added examples/benchmark/public/favicon.ico
Binary file not shown.
100 changes: 100 additions & 0 deletions examples/benchmark/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<!DOCTYPE html>
<!--
@webtoon/psd
Copyright 2021-present NAVER WEBTOON
MIT License
-->
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PSD Benchmark Runner - @webtoon/psd</title>
</head>
<body>
<noscript>You need JavaScript to run benchmarks</noscript>
<main id="app" class="app">
<header class="app__section app-header">
<h1>
Benchmark runner for
<span class="webtoon-psd-name">@webtoon/psd</span> and other parsers
</h1>
<p>
Compare the performance of
<span class="webtoon-psd-name">@webtoon/psd</span> and other
JavaScript PSD parsers (<a
target="_blank"
href="https://github.com/meltingice/psd.js"
rel="noopener"
>PSD.js</a
>,
<a
target="_blank"
href="https://github.com/Agamnentzar/ag-psd"
rel="noopener"
>ag-psd</a
>) by selecting a PSD file on your device.
</p>
<p>
(Note: Some features from the PSD file spec are not supported by all
parsers. As such, this page should be treated as a rough indicator of
performance.)
</p>
</header>
<div class="app__section control-panel">
<div>
<label>
Select a PSD file:&nbsp;
<input id="file-input" type="file" accept=".psd,.psb" />
</label>
</div>
<div>
<label>
# of times to parse the file:&nbsp;
<input
id="repeat-count-input"
type="number"
min="0"
max="1000"
value="3"
/>
</label>
</div>
<div>
<label>
<input id="apply-opacity-checkbox" type="checkbox" />
Apply opacity when decoding (supported by
<span class="webtoon-psd-name">@webtoon/psd</span> only)
</label>
</div>
</div>
<div class="app__section progress-panel">
<label class="progress-panel__description" for="progressbar"
>Select a file</label
>
<progress
class="progress-panel__progressbar"
id="progressbar"
></progress>
</div>
<div class="app__section result-panel" style="display: none">
<div class="result-panel__spinner-container">
<div class="progress-spinner" data-state="spinning"></div>
</div>
<div class="result-panel__table-container"></div>
<canvas id="result-chart-canvas" width="400" height="100">
Cannot display chart because your browser does not support canvas
</canvas>
</div>
<div class="app__section error-panel" style="display: none">
<div class="error-panel__message"></div>
</div>
</main>
<!--
The browser bundle of PSD.js is available only through Bower and GitHub.
Since I don't want to use Bower, let's hotlink directly to the latest
version of the bundle on GitHub instead.
-->
<script src="https://cdn.jsdelivr.net/gh/meltingice/psd.js@master/dist/psd.js"></script>
</body>
</html>
109 changes: 109 additions & 0 deletions examples/benchmark/src/bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// @webtoon/psd
// Copyright 2021-present NAVER WEBTOON
// MIT License

import * as AgPsd from "ag-psd";
import Psd from "../../../src";

// Use the require() function provided by the browser bundle of PSD.js
// Because TypeScript prevents us from calling `require()` inside an ESM module,
// we must call `globalThis.require()` instead
const PsdJs = globalThis.require("psd") as typeof import("./psd-js");

export interface BenchmarkResult {
parseTime: number;
imageRenderTime: number;
layerRenderTime: number;
}

export function benchmarkPsdJs(arrayBuffer: ArrayBuffer): BenchmarkResult {
const parseBegin = performance.now();
const psd = new PsdJs(new Uint8Array(arrayBuffer));
psd.parse();
psd.tree().export(); // trigger all getters
const parseEnd = performance.now();

const imageRenderBegin = performance.now();
psd.image.pixelData; // trigger getter
const imageRenderEnd = performance.now();

const layerRenderBegin = performance.now();
// trigger getters
psd
.tree()
.descendants()
.forEach((node) => node.type === "layer" && node.layer.image.pixelData);
const layerRenderEnd = performance.now();

return {
parseTime: parseEnd - parseBegin,
imageRenderTime: imageRenderEnd - imageRenderBegin,
layerRenderTime: layerRenderEnd - layerRenderBegin,
};
}

export function benchmarkPsdTs(
arrayBuffer: ArrayBuffer,
options: {applyOpacity: boolean}
): BenchmarkResult {
const parseBegin = performance.now();
const psd = Psd.parse(arrayBuffer);
const parseEnd = performance.now();

const imageRenderBegin = performance.now();
psd.composite(options.applyOpacity);
const imageRenderEnd = performance.now();

const layerRenderBegin = performance.now();
psd.layers.forEach((layer) => layer.composite(options.applyOpacity));
const layerRenderEnd = performance.now();

return {
parseTime: parseEnd - parseBegin,
imageRenderTime: imageRenderEnd - imageRenderBegin,
layerRenderTime: layerRenderEnd - layerRenderBegin,
};
}

export function benchmarkAgPsd(arrayBuffer: ArrayBuffer): BenchmarkResult {
const parseBegin = performance.now();
AgPsd.readPsd(arrayBuffer, {
skipCompositeImageData: true,
skipLayerImageData: true,
skipThumbnail: true,
useImageData: true,
});
const parseEnd = performance.now();

// ag-psd does not provide APIs for decoding the merged image separately.
// Instead, we measure (parse time + merged image decoding time) and subtract
// the parse time. This is probably inaccurate.
const parseAndImageRenderBegin = performance.now();
AgPsd.readPsd(arrayBuffer, {
skipLayerImageData: true,
skipThumbnail: true,
useImageData: true,
});
const parseAndImageRenderEnd = performance.now();

// ag-psd does not provide APIs for decoding each layer image separately.
// Instead, we measure (parse time + all layer decoding time) and subtract
// the parse time. This is probably inaccurate.
const parseAndLayerRenderBegin = performance.now();
AgPsd.readPsd(arrayBuffer, {
skipCompositeImageData: true,
skipThumbnail: true,
useImageData: true,
});
const parseAndLayerRenderEnd = performance.now();

const parseTime = parseEnd - parseBegin;

return {
parseTime,
imageRenderTime:
parseAndImageRenderEnd - parseAndImageRenderBegin - parseTime,
layerRenderTime:
parseAndLayerRenderEnd - parseAndLayerRenderBegin - parseTime,
};
}
81 changes: 81 additions & 0 deletions examples/benchmark/src/chart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// @webtoon/psd
// Copyright 2021-present NAVER WEBTOON
// MIT License

import {
BarController,
BarElement,
CategoryScale,
Chart,
Legend,
LinearScale,
} from "chart.js";

// Initialize Chart.js
Chart.register(BarController, BarElement, CategoryScale, Legend, LinearScale);

const COLOR_SCHEME = {
backgroundColor: [
"rgba(245, 121, 58, 0.5)",
"rgba(169, 90, 161, 0.5)",
"rgba(133, 192, 249, 0.5)",
],
borderColor: [
"rgba(245, 121, 58, 1)",
"rgba(169, 90, 161, 1)",
"rgba(133, 192, 249, 1)",
],
};

export function drawChart(
canvas: HTMLCanvasElement,
{
categories,
measurements,
}: {
categories: string[];
measurements: {parserName: string; values: number[]}[];
}
) {
/** Library names */
const parserNames = measurements.map(({parserName}) => parserName);
const datasets = categories.map((category, categoryIndex) => ({
label: category,
data: measurements.map(({values}) => values[categoryIndex]),
backgroundColor: COLOR_SCHEME.backgroundColor[categoryIndex],
borderColor: COLOR_SCHEME.borderColor[categoryIndex],
borderWidth: 1,
barPercentage: 0.6,
}));

const chart = Chart.getChart(canvas);
if (!chart) {
new Chart(canvas, {
type: "bar",
data: {
labels: parserNames,
datasets,
},
options: {
indexAxis: "y",
plugins: {
legend: {
display: true,
},
},
scales: {
x: {
stacked: true,
},
y: {
stacked: true,
},
},
},
});
} else {
chart.data.labels = parserNames;
chart.data.datasets = datasets;
chart.update();
}
}
Loading

0 comments on commit 9bc26f9

Please sign in to comment.