Skip to content

Commit 5967ce9

Browse files
committed
Move benchmark top-level
1 parent f410d36 commit 5967ce9

5 files changed

Lines changed: 138 additions & 4 deletions

File tree

benchmark/decode-quality.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { readdirSync, statSync } from 'node:fs';
2+
import { dirname, join as pjoin } from 'node:path';
3+
import { performance } from 'node:perf_hooks';
4+
import { fileURLToPath } from 'node:url';
5+
import decodeQR from '../src/decode.ts';
6+
// Reuse the curated expectations from decode tests; should.runWhen keeps tests idle on import.
7+
import { DECODED, DECODE_VECTOR_EXCLUDE } from '../test/decode.test.ts';
8+
import { isDecodeImage, readImage } from '../test/utils.ts';
9+
10+
const _dirname = dirname(fileURLToPath(import.meta.url));
11+
const DETECTION_PATH = pjoin(_dirname, '..', 'test', 'vectors', 'boofcv-v3', 'detection');
12+
13+
const listFiles = (path, isDir = false) =>
14+
readdirSync(path)
15+
.filter((i) => statSync(`${path}/${i}`).isDirectory() === isDir)
16+
.sort();
17+
18+
const percent = (value, total) => (total === 0 ? 'n/a' : `${((100 * value) / total).toFixed(1)}%`);
19+
const millis = (value) => `${Math.round(value)}ms`;
20+
21+
const select = (envName, values) => {
22+
const raw = process.env[envName];
23+
if (!raw) return values;
24+
const selected = raw
25+
.split(',')
26+
.map((s) => s.trim())
27+
.filter(Boolean);
28+
for (const value of selected) {
29+
if (!values.includes(value)) throw new Error(`unknown ${envName} value=${value}`);
30+
}
31+
return selected;
32+
};
33+
34+
function vectorFiles(category) {
35+
const dir = pjoin(DETECTION_PATH, category);
36+
return listFiles(dir)
37+
.filter(isDecodeImage)
38+
.filter((file) => !DECODE_VECTOR_EXCLUDE.includes(`${category}/${file}`))
39+
.map((file) => ({
40+
category,
41+
file,
42+
path: pjoin('detection', category, file),
43+
expected: DECODED[category]?.[file],
44+
}));
45+
}
46+
47+
function runCategory(files) {
48+
const stats = {
49+
files: 0,
50+
expected: 0,
51+
matched: 0,
52+
wrong: 0,
53+
missed: 0,
54+
unknownDecoded: 0,
55+
errors: 0,
56+
ms: 0,
57+
};
58+
const started = performance.now();
59+
for (const vector of files) {
60+
stats.files++;
61+
if (vector.expected) stats.expected++;
62+
let decoded;
63+
try {
64+
decoded = decodeQR(readImage(vector.path), { moreEffort: true });
65+
} catch {
66+
stats.errors++;
67+
}
68+
if (vector.expected) {
69+
if (vector.expected.includes(decoded)) stats.matched++;
70+
else if (decoded === undefined) stats.missed++;
71+
else stats.wrong++;
72+
} else if (decoded !== undefined) {
73+
stats.unknownDecoded++;
74+
}
75+
}
76+
stats.ms = performance.now() - started;
77+
return stats;
78+
}
79+
80+
function printRow(category, stats) {
81+
const msPerImage = stats.files === 0 ? 0 : stats.ms / stats.files;
82+
const quality = stats.matched + stats.unknownDecoded;
83+
console.log(`${category},${percent(quality, stats.files)},${millis(msPerImage)}`);
84+
}
85+
86+
function main() {
87+
const categories = select('QR_QUALITY_CATEGORIES', listFiles(DETECTION_PATH, true));
88+
for (const category of categories) {
89+
const files = vectorFiles(category);
90+
if (files.length === 0) continue;
91+
printRow(category, runCategory(files));
92+
}
93+
}
94+
95+
main();

benchmark/index.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { bench } from '@paulmillr/jsbt/bench.js';
2+
import { deepStrictEqual } from 'node:assert';
3+
import decodeQR from '../src/decode.ts';
4+
import { Bitmap, encodeQR } from '../src/index.ts';
5+
6+
const section = (name) => console.log(`\n# ${name}`);
7+
8+
function imageFromText(text, opts = {}) {
9+
const { scale = 4, ...qrOpts } = opts;
10+
const raw = encodeQR(text, 'raw', { border: 4, ecc: 'medium', ...qrOpts });
11+
return new Bitmap(raw.length, raw).scale(scale).toImage();
12+
}
13+
14+
const smallText = 'HELLO WORLD';
15+
const largeText = 'H'.repeat(768);
16+
const smallImage = imageFromText(smallText, { version: 1, scale: 4 });
17+
const largeImage = imageFromText(largeText, { scale: 2 });
18+
19+
async function main() {
20+
section('encode');
21+
await bench('ascii small', () => encodeQR(smallText, 'ascii', { ecc: 'medium' }));
22+
await bench('gif small', () => encodeQR(smallText, 'gif', { ecc: 'medium' }));
23+
await bench('svg small', () => encodeQR(smallText, 'svg', { ecc: 'medium' }));
24+
25+
section('encode large');
26+
await bench('raw large', () => encodeQR(largeText, 'raw', { ecc: 'medium' }));
27+
await bench('ascii large', () => encodeQR(largeText, 'ascii', { ecc: 'medium' }));
28+
29+
section('decode generated');
30+
await bench('v1 scale4', () => deepStrictEqual(decodeQR(smallImage), smallText));
31+
await bench('large scale2', () => deepStrictEqual(decodeQR(largeImage), largeText));
32+
}
33+
34+
main();
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ import jsqr from 'jsqr';
1212
import { fileURLToPath } from 'node:url';
1313
import * as qrcodeGenerator from 'qrcode-generator';
1414

15+
const _dirname = dirname(fileURLToPath(import.meta.url));
1516
const decodeExp = 'https://www.surveymonkey.com/s/TheClubatLAS_T3';
16-
const decodeJPG = jpeg.decode(readFileSync('../vectors/boofcv-v3/detection/blurred/image007.jpg'));
17+
const decodeJPG = jpeg.decode(
18+
readFileSync(_dirname + '/../../test/vectors/boofcv-v3/detection/blurred/image007.jpg')
19+
);
1720

1821
// Compared to other JS libraries:
1922
// - Don't work: [jsQR](https://github.com/cozmo/jsQR) is dead, [zxing-js](https://github.com/zxing-js/) is [dead](https://github.com/zxing-js/library/commit/b797504c25454db32aa2db410e6502b6db12a401), [qr-scanner](https://github.com/nimiq/qr-scanner/) uses jsQR, doesn't work outside of browser, [qcode-decoder](https://github.com/cirocosta/qcode-decoder) broken version of jsQR, doesn't work outside of browser
@@ -97,7 +100,7 @@ async function main() {
97100

98101
section('Decoding quality');
99102
const _dirname = dirname(fileURLToPath(import.meta.url));
100-
const DETECTION_PATH = pjoin(_dirname, '..', 'vectors', 'boofcv-v3', 'detection');
103+
const DETECTION_PATH = pjoin(_dirname, '..', '..', 'test', 'vectors', 'boofcv-v3', 'detection');
101104

102105
for (const category of listFiles(DETECTION_PATH, true)) {
103106
const DIR_PATH = `${DETECTION_PATH}/${category}`;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"main": "index.js",
77
"type": "module",
88
"scripts": {
9-
"bench": "node index.ts"
9+
"benchmark": "node index.ts"
1010
},
1111
"keywords": [],
1212
"author": "",

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@
3535
"build": "tsc",
3636
"bundle": "npx --no @paulmillr/jsbt bundle test/build",
3737
"check": "npx --no @paulmillr/jsbt check",
38-
"bench": "cd test/benchmark && npm ci && node index.ts",
38+
"benchmark": "node benchmark/index.ts",
39+
"benchmark:quality": "node benchmark/decode-quality.ts",
40+
"benchmark:thirdparty": "cd benchmark/thirdparty && npm ci && node index.ts",
3941
"format": "prettier --write src",
4042
"test": "node --no-warnings test/index.ts",
4143
"test:bun": "bun test/index.ts",

0 commit comments

Comments
 (0)