diff --git a/examples/benchmark/public/index.html b/examples/benchmark/public/index.html index 4233571..6108349 100644 --- a/examples/benchmark/public/index.html +++ b/examples/benchmark/public/index.html @@ -42,11 +42,35 @@

-
- -
-
- +
Run benchmark with PSD file:
+
+
+ +
+
+ + +
+
+ +
+
+ + +
diff --git a/examples/benchmark/src/main.ts b/examples/benchmark/src/main.ts index ea47f33..dc4408d 100644 --- a/examples/benchmark/src/main.ts +++ b/examples/benchmark/src/main.ts @@ -27,22 +27,51 @@ const benchmarkSetup: BenchmarkTaskSetup[] = [ let appState = initialAppState; +async function runBenchmark() { + await raf(() => render(appState)); + + while (appState.isRunning()) { + // eslint-disable-next-line @typescript-eslint/no-empty-function + await raf(() => {}); + appState = await appState.runNextSubtask(); + await raf(() => render(appState)); + } +} + // Initial render render(appState); initialize({ + onUseDefaultPsdCheckboxClick() { + appState = appState.updateOptions({preferDefaultPsd: true}); + render(appState); + }, + onUseUploadedPsdCheckboxClick() { + appState = appState.updateOptions({preferDefaultPsd: false}); + render(appState); + }, + async onFileInputChange(file) { if (!file) return; try { appState = appState.start(benchmarkSetup, file); - await raf(() => render(appState)); + runBenchmark(); + } catch (error) { + appState = appState.setError(error); + render(appState); + } + }, + + async onUseDefaultPsdFileButtonClick() { + if (!appState.defaultPsdFileData) { + appState = appState.setError("Default PSD file not loaded yet"); + render(appState); + return; + } - while (appState.isRunning()) { - // eslint-disable-next-line @typescript-eslint/no-empty-function - await raf(() => {}); - appState = await appState.runNextSubtask(); - await raf(() => render(appState)); - } + try { + appState = appState.startWithDefaultPsdFile(benchmarkSetup); + runBenchmark(); } catch (error) { appState = appState.setError(error); render(appState); @@ -60,6 +89,17 @@ initialize({ }, }); +// Load default PSD file +(async () => { + try { + appState = await appState.loadDefaultPsdFile(); + render(appState); + } catch (error) { + appState = appState.setError(error); + render(appState); + } +})(); + /** * `window.requestAnimationFrame` promisified * @returns diff --git a/examples/benchmark/src/model.ts b/examples/benchmark/src/model.ts index 0d5c92e..2283fa3 100644 --- a/examples/benchmark/src/model.ts +++ b/examples/benchmark/src/model.ts @@ -32,6 +32,8 @@ export class AppState { readonly psdFileName: string | null; readonly psdFileData: ArrayBuffer | null; readonly error: string | null; + readonly defaultPsdFileUrl: URL; + readonly defaultPsdFileData: ArrayBuffer | null; constructor({ tasks, @@ -41,6 +43,8 @@ export class AppState { psdFileData, psdFileName, error, + defaultPsdFileUrl, + defaultPsdFileData, }: AppStateProperties) { this.tasks = Object.freeze([...tasks]); this.benchmarkResults = Object.freeze([...benchmarkResults]); @@ -51,6 +55,8 @@ export class AppState { this.psdFileName = psdFileName; this.psdFileData = psdFileData; this.error = error; + this.defaultPsdFileUrl = defaultPsdFileUrl; + this.defaultPsdFileData = defaultPsdFileData; Object.freeze(this); } @@ -64,6 +70,8 @@ export class AppState { psdFileData = this.psdFileData, psdFileName = this.psdFileName, error = this.error, + defaultPsdFileUrl = this.defaultPsdFileUrl, + defaultPsdFileData = this.defaultPsdFileData, } = properties; return new AppState({ tasks, @@ -73,6 +81,8 @@ export class AppState { psdFileData, psdFileName, error, + defaultPsdFileUrl, + defaultPsdFileData, }); } @@ -140,6 +150,28 @@ export class AppState { }); } + startWithDefaultPsdFile(setups: readonly BenchmarkTaskSetup[]) { + if (!this.defaultPsdFileData) { + throw new Error("Default PSD file has not been loaded yet"); + } + + return this.#update({ + tasks: setups.map( + (setup) => + new BenchmarkTask({ + libraryName: setup.libraryName, + benchmarkCallback: setup.benchmarkCallback, + remainingTrialCount: this.options.trialCount, + }) + ), + currentTaskTrialMeasurements: [], + benchmarkResults: [], + psdFileName: this.defaultPsdFileUrl.pathname, + psdFileData: this.defaultPsdFileData, + error: null, + }); + } + async runNextSubtask() { if (!this.isRunning()) { throw new Error("Cannot run next trial because the app has halted"); @@ -198,12 +230,14 @@ export class AppState { const { trialCount = this.options.trialCount, shouldApplyOpacity = this.options.shouldApplyOpacity, + preferDefaultPsd = this.options.preferDefaultPsd, } = newOptions; return this.#update({ options: new BenchmarkOptions({ trialCount, shouldApplyOpacity, + preferDefaultPsd, }), }); } @@ -213,16 +247,32 @@ export class AppState { error: error === null ? error : String(error), }); } + + async loadDefaultPsdFile() { + if (this.defaultPsdFileData) { + throw new Error(`${this.defaultPsdFileUrl} has already been loaded`); + } + + // eslint-disable-next-line compat/compat + const response = await fetch(String(this.defaultPsdFileUrl)); + return this.#update({ + defaultPsdFileData: await response.arrayBuffer(), + }); + } } export const initialAppState = new AppState({ tasks: [], benchmarkResults: [], currentTaskTrialMeasurements: [], - options: {trialCount: 3, shouldApplyOpacity: false}, + options: {trialCount: 3, shouldApplyOpacity: false, preferDefaultPsd: true}, psdFileName: null, psdFileData: null, error: null, + // Webpack will resolve this as a resource asset + // eslint-disable-next-line compat/compat + defaultPsdFileUrl: new URL("../../node/example.psd", import.meta.url), + defaultPsdFileData: null, }); export type Task = LoadFileTask | BenchmarkTask; @@ -365,8 +415,13 @@ export class BenchmarkOptions { readonly trialCount: number; /** Whether to apply opacity when decoding image data */ readonly shouldApplyOpacity: boolean; + readonly preferDefaultPsd: boolean; - constructor({trialCount, shouldApplyOpacity}: BenchmarkOptionsProperties) { + constructor({ + trialCount, + shouldApplyOpacity, + preferDefaultPsd, + }: BenchmarkOptionsProperties) { if (!(Number.isSafeInteger(trialCount) && trialCount > 0)) { throw new Error( `trialCount must be a positive safe integer (got ${trialCount})` @@ -375,6 +430,7 @@ export class BenchmarkOptions { this.trialCount = trialCount; this.shouldApplyOpacity = shouldApplyOpacity; + this.preferDefaultPsd = preferDefaultPsd; Object.freeze(this); } diff --git a/examples/benchmark/src/style.css b/examples/benchmark/src/style.css index 3c08261..d459648 100644 --- a/examples/benchmark/src/style.css +++ b/examples/benchmark/src/style.css @@ -65,6 +65,20 @@ font-size: 14px; } +.control-panel__psd-source-grid { + display: grid; + grid-template-columns: max-content 1fr; + gap: 16px 4px; +} + +#use-sample-psd-button { + margin-left: 4px; +} + +#file-input { + width: 160px; +} + .progress-panel { display: flex; flex-direction: column; diff --git a/examples/benchmark/src/views/control-panel.ts b/examples/benchmark/src/views/control-panel.ts index 3b3ceac..478508c 100644 --- a/examples/benchmark/src/views/control-panel.ts +++ b/examples/benchmark/src/views/control-panel.ts @@ -6,14 +6,33 @@ import type {AppState} from "../model"; import {findElement} from "./common"; export function initControlPanel({ + onUseDefaultPsdCheckboxClick, + onUseUploadedPsdCheckboxClick, onFileInputChange, + onUseDefaultPsdFileButtonClick, onTrialCountChange, onShouldApplyOpacityChange, }: { + onUseDefaultPsdCheckboxClick?: () => void; + onUseUploadedPsdCheckboxClick?: () => void; onFileInputChange?: (file: File | null) => void; + onUseDefaultPsdFileButtonClick?: () => void; onTrialCountChange?: (value: number) => void; onShouldApplyOpacityChange?: (value: boolean) => void; }) { + const {useDefaultPsdCheckbox, useUploadedPsdCheckbox} = + getPsdSourceCheckboxes(); + useDefaultPsdCheckbox.addEventListener("click", () => { + onUseDefaultPsdCheckboxClick?.(); + }); + useUploadedPsdCheckbox.addEventListener("click", () => { + onUseUploadedPsdCheckboxClick?.(); + }); + + getRunWithDefaultPsdButton().addEventListener("click", () => + onUseDefaultPsdFileButtonClick?.() + ); + getFileInput().addEventListener("change", (event) => { const fileInput = event.target as ReturnType; const file = fileInput.files?.[0] ?? null; @@ -39,8 +58,32 @@ export function initControlPanel({ } export function renderControlPanel(appState: AppState) { + const {useDefaultPsdCheckbox, useUploadedPsdCheckbox} = + getPsdSourceCheckboxes(); + if (appState.options.preferDefaultPsd) { + useDefaultPsdCheckbox.checked = true; + } else { + useUploadedPsdCheckbox.checked = true; + } + + useDefaultPsdCheckbox.disabled = appState.isRunning(); + useUploadedPsdCheckbox.disabled = appState.isRunning(); + + const samplePsdDownloadAnchor = findElement( + "a#sample-psd-download-link", + "'Download sample PSD' anchor" + ); + samplePsdDownloadAnchor.href = String(appState.defaultPsdFileUrl); + + const isLoadingDefaultPsdFile = appState.defaultPsdFileData === null; + getRunWithDefaultPsdButton().disabled = + !appState.options.preferDefaultPsd || + isLoadingDefaultPsdFile || + appState.isRunning(); + const fileInput = getFileInput(); - fileInput.disabled = appState.isRunning(); + fileInput.disabled = + appState.options.preferDefaultPsd || appState.isRunning(); const trialCountInput = getTrialCountInput(); trialCountInput.valueAsNumber = appState.options.trialCount; @@ -51,6 +94,26 @@ export function renderControlPanel(appState: AppState) { applyOpacityCheckbox.disabled = appState.isRunning(); } +function getPsdSourceCheckboxes() { + return { + useDefaultPsdCheckbox: findElement( + "input#use-sample-psd-checkbox", + "'Use sample PSD' checkbox" + ), + useUploadedPsdCheckbox: findElement( + "input#use-uploaded-psd-checkbox", + "'Use uploaded PSD' checkbox" + ), + }; +} + +function getRunWithDefaultPsdButton() { + return findElement( + "button#use-sample-psd-button", + "'Run with sample PSD' button element" + ); +} + function getFileInput() { return findElement( "input#file-input",