Skip to content

Commit b4cc657

Browse files
committed
Add new benchmarks
1 parent cfc5018 commit b4cc657

25 files changed

Lines changed: 1926 additions & 44 deletions

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ on:
44
types: [created]
55
jobs:
66
release-js:
7-
name: jsbt v0.5.2
8-
uses: paulmillr/jsbt/.github/workflows/release.yml@3b6bf563e416e9c6047db19410988922084e1e92
7+
name: jsbt v0.6.0
8+
uses: paulmillr/jsbt/.github/workflows/release.yml@7c25cb0b5eadac32bfedbb3b60ca0bd4289791f1
99
permissions:
1010
contents: read
1111
id-token: write

benchmark/1-hashes-ciphers.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,21 @@ function rbuf(n: number) {
1414
x ^= x << 5;
1515
out[i] = x & 0xff;
1616
}
17-
SEED = x >>> 0; // guaranteed persisted
17+
SEED = x >>> 0;
1818
return out;
1919
}
2020

2121
const BUFFERS = {
2222
'32b': rbuf(32),
2323
'1mb': rbuf(1024 * 1024),
24-
'10mb': rbuf(1024 * 1024 * 10)
24+
'10mb': rbuf(1024 * 1024 * 10),
2525
};
2626

2727
async function main() {
2828
const data = BUFFERS['1mb'];
29-
const opts = { unit: 'mb', multiplier: 1 };
30-
const cdata = BUFFERS['10mb']
31-
const copts = { unit: 'mb', multiplier: 10 };
29+
const opts = { bytes: data.byteLength };
30+
const cdata = BUFFERS['10mb'];
31+
const copts = { bytes: cdata.byteLength };
3232
const libs = wasm;
3333

3434
// prettier-ignore
@@ -39,7 +39,7 @@ async function main() {
3939
const { chacha20poly1305, gcm, gcmsiv, chacha20, ecb, cbc, ctr } = libs;
4040
// prettier-ignore
4141
const key = rbuf(32), n12 = rbuf(12), n16 = rbuf(16);
42-
// warm-up
42+
// Warm up hot paths before measuring.
4343
for (let i = 0; i < 1000; i++) libs.sha256(data);
4444
for (let i = 0; i < 1000; i++) chacha20poly1305(key, n12).encrypt(cdata);
4545

benchmark/2-parallel.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ function rbuf(n: number) {
1414
x ^= x << 5;
1515
out[i] = x & 0xff;
1616
}
17-
SEED = x >>> 0; // guaranteed persisted
17+
SEED = x >>> 0;
1818
return out;
1919
}
2020

@@ -29,9 +29,9 @@ const BUFFERS = {
2929
async function main_threads() {
3030
const CHUNKS = 48;
3131
const data = BUFFERS['1mb'];
32-
const opts = { unit: 'mb', multiplier: CHUNKS };
33-
const cdata = BUFFERS['10mb']
34-
const copts = { unit: 'mb', multiplier: 10 };
32+
const opts = { bytes: data.byteLength * CHUNKS };
33+
const cdata = BUFFERS['10mb'];
34+
const copts = { bytes: cdata.byteLength };
3535
const libs = wasm_threads;
3636

3737
const chunks = Array(CHUNKS).fill(data);
@@ -42,9 +42,9 @@ async function main_threads() {
4242
const { chacha20poly1305, gcm, gcmsiv, chacha20, ecb, cbc, ctr } = libs;
4343
// prettier-ignore
4444
const key = rbuf(32), n12 = rbuf(12), n16 = rbuf(16), n24 = rbuf(24);
45+
// Warm up workers and cipher constructors before measuring.
4546
let start = Date.now();
4647
while (5000 > Date.now() - start) {
47-
// warm-up
4848
await libs.sha256.parallel(chunks);
4949
await chacha20poly1305(key, n12).encrypt(cdata);
5050
}
@@ -55,8 +55,9 @@ async function main_threads() {
5555
}
5656
await bench(`blake3 ${CHUNKS}x1mb`, () => libs.blake3.parallel(chunks), opts);
5757
const b3buf = [BUFFERS['1gb']];
58-
const b3o = { unit: 'mb', multiplier: 1024 }
59-
await bench('blake3 1x1gb', () => libs.blake3.parallel(b3buf), b3o);
58+
await bench('blake3 1x1gb', () => libs.blake3.parallel(b3buf), {
59+
bytes: b3buf[0].byteLength,
60+
});
6061
const hashes2 = ['ripemd160', 'md5', 'sha1'];
6162
for (const title of hashes2) {
6263
const hash = libs[title];

benchmark/3-fastest.ts

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,18 @@ function rbuf(n: number) {
1212
x ^= x << 5;
1313
out[i] = x & 0xff;
1414
}
15-
SEED = x >>> 0; // guaranteed persisted
15+
SEED = x >>> 0;
1616
return out;
1717
}
1818

1919
const BUFFERS = {
20-
'32b': { data: rbuf(32), multiplier: 1/32768 },
21-
'64b': { data: rbuf(64), multiplier: 1/16384 },
22-
'1kb': { data: rbuf(1024), multiplier: 1/1024 },
23-
'64kb': { data: rbuf(1024 * 64), multiplier: 1/16 },
24-
'1mb': { data: rbuf(1024 * 1024), multiplier: 1 },
25-
'10mb': { data: rbuf(1024 * 1024 * 10), multiplier: 10 },
26-
'100mb': { data: rbuf(1024 * 1024 * 100), multiplier: 100 },
20+
'32b': rbuf(32),
21+
'64b': rbuf(64),
22+
'1kb': rbuf(1024),
23+
'64kb': rbuf(1024 * 64),
24+
'1mb': rbuf(1024 * 1024),
25+
'10mb': rbuf(1024 * 1024 * 10),
26+
'100mb': rbuf(1024 * 1024 * 100),
2727
};
2828
const CHUNKS_ = [1, 2, 4, 8, 16, 32, 40, 48, 56, 64, 72, 80, 88, 96, 128];
2929
const now = () => process.hrtime.bigint();
@@ -33,7 +33,7 @@ async function main_threads() {
3333
let res;
3434
for (const hashName of hashes) {
3535
const hash = libs[hashName];
36-
for (let [bufName, { data, multiplier }] of Object.entries(BUFFERS)) {
36+
for (const [bufName, data] of Object.entries(BUFFERS)) {
3737
for (let chunk of CHUNKS_) {
3838
if (bufName === '100mb' && chunk > 16) continue;
3939
const input = Array(chunk).fill(data);
@@ -43,20 +43,12 @@ async function main_threads() {
4343
const diff = now() - start;
4444
const num = Number(diff);
4545
const SECOND = 10 ** 9;
46-
const perSec = Math.round((SECOND * (multiplier * chunk)) / num);
47-
console.log(`${perSec},${hashName},${chunk}x,${bufName}`)
46+
const mib = data.byteLength / (1024 * 1024);
47+
const perSec = Math.round((SECOND * mib * chunk) / num);
48+
console.log(`${perSec},${hashName},${chunk}x,${bufName}`);
4849
}
4950
}
5051
}
51-
52-
// console.log(`# ciphers, input: 10mb, +threads`);
53-
// await bench('chacha20poly1305', () => chacha20poly1305(key, n12).encrypt(cdata), copts);
54-
// await bench('aes-gcm-256', () => gcm(key, n12).encrypt(cdata), copts);
55-
// await bench('aes-gcm-siv-256', () => gcmsiv(key, n12).encrypt(cdata), copts);
56-
// await bench('chacha20', () => chacha20(key, n12).encrypt(cdata), copts);
57-
// await bench('aes-ecb-256', () => ecb(key).encrypt(cdata), copts);
58-
// await bench('aes-cbc-256', () => cbc(key, n16).encrypt(cdata), copts);
59-
// await bench('aes-ctr-256', () => ctr(key, n16).encrypt(cdata), copts);
6052
}
6153

6254
main_threads();

benchmark/internal/aeskw.ts

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import compare from '@paulmillr/jsbt/bench-compare.js';
2+
3+
import * as js from '../../src/targets/js/index.ts';
4+
import * as wasm from '../../src/targets/wasm/index.ts';
5+
import * as wasm_threads from '../../src/targets/wasm_threads/index.ts';
6+
import { WP } from '../../src/workers.ts';
7+
import { NOBLE } from '../../test/noble-all.ts';
8+
import { buf } from './utils-ciphers.ts';
9+
10+
// AES-KW/AES-KWP have atypical length rules (e.g. KW forbids 0-byte and 8-byte messages),
11+
// so we keep a local cross-validator instead of generalizing `utils-ciphers.ts`.
12+
const eql = (a: Uint8Array, b: Uint8Array, msg?: string) => {
13+
if (a.length !== b.length) throw new Error(`u8a.eql: length ${a.length}!==${b.length} (${msg})`);
14+
for (let i = 0; i < a.length; i++) {
15+
if (a[i] !== b[i]) throw new Error(`u8a.eql: a[${i}](${a[i]})!==b[${i}](${b[i]}), ${msg}`);
16+
}
17+
};
18+
const crossValidateAesKw = async (
19+
title: string,
20+
buffers: Record<string, Uint8Array>,
21+
ciphers: any,
22+
allowEnc: (len: number) => boolean
23+
) => {
24+
const bufs = Object.values(buffers).slice();
25+
const bufMap = new Map(Object.entries(buffers).map(([k, v]) => [v, k]));
26+
for (let i = 0; i < 2048; i++) {
27+
if (!allowEnc(i)) continue;
28+
bufs.push(buf(i));
29+
}
30+
31+
const res: Record<string, Uint8Array> = {};
32+
for (const msg of bufs) {
33+
const bname = bufMap.get(msg) || `${msg.length}`;
34+
const orig = msg.slice();
35+
let encrypted: Uint8Array | undefined;
36+
const opts = ciphers.options;
37+
for (const [lib, fn] of Object.entries(ciphers)) {
38+
if (lib === 'options') continue;
39+
if (encrypted === undefined) encrypted = await fn.encrypt(msg, opts);
40+
else
41+
eql(encrypted, await fn.encrypt(msg, opts), `${title}/${bname}: encrypt verify (${lib})`);
42+
eql(msg, orig, `${title}/${bname}: encrypt mutates buffer (${lib})`);
43+
eql(await fn.decrypt(encrypted, opts), msg, `${title}/${bname}: decrypt verify (${lib})`);
44+
}
45+
const bufName = bufMap.get(msg);
46+
if (bufName) res[bufName] = encrypted!;
47+
}
48+
return res;
49+
};
50+
51+
const benchBuf = (n: number, seed = 0x9e3779b9) => {
52+
// Sequential buffers can bias AES table/cache access patterns.
53+
// Use deterministic pseudo-random data so noble/awasm are compared on less structured input.
54+
const out = new Uint8Array(n);
55+
let x = seed >>> 0;
56+
for (let i = 0; i < n; i++) {
57+
x ^= x << 13;
58+
x ^= x >>> 17;
59+
x ^= x << 5;
60+
out[i] = x & 0xff;
61+
}
62+
return out;
63+
};
64+
65+
const BUFFERS = {
66+
'64B': benchBuf(64),
67+
'1MB': benchBuf(1024 * 1024),
68+
};
69+
70+
const addAesKw = (algoName: 'aeskw' | 'aeskwp') => {
71+
const res: Record<string, { encrypt: (b: Uint8Array, o: any) => Uint8Array; decrypt: any }> = {};
72+
// Keep noble first so diff baselines match "noble-ciphers" by default.
73+
for (const [name, lib] of Object.entries({ noble: NOBLE, wasm, wasm_threads, js })) {
74+
const fn = (lib as any)[algoName];
75+
res[name] = {
76+
encrypt: (b, opts) => fn(opts.key).encrypt(b),
77+
decrypt: (b, opts) => fn(opts.key).decrypt(b),
78+
};
79+
}
80+
return res;
81+
};
82+
83+
export const AESKW = {
84+
Wrap: {
85+
aeskw: {
86+
NoPadding: {
87+
128: {
88+
options: { key: buf(16), blockSize: 8 },
89+
...addAesKw('aeskw'),
90+
},
91+
192: {
92+
options: { key: buf(24), blockSize: 8 },
93+
...addAesKw('aeskw'),
94+
},
95+
256: {
96+
options: { key: buf(32), blockSize: 8 },
97+
...addAesKw('aeskw'),
98+
},
99+
},
100+
},
101+
aeskwp: {
102+
Padding: {
103+
128: {
104+
options: { key: buf(16) },
105+
...addAesKw('aeskwp'),
106+
},
107+
192: {
108+
options: { key: buf(24) },
109+
...addAesKw('aeskwp'),
110+
},
111+
256: {
112+
options: { key: buf(32) },
113+
...addAesKw('aeskwp'),
114+
},
115+
},
116+
},
117+
},
118+
};
119+
120+
export async function main() {
121+
await WP.waitOnline();
122+
const crossFilter = process.env.CROSS_FILTER;
123+
124+
const encrypted: Record<string, Record<string, Uint8Array>> = {};
125+
for (const [type, algos] of Object.entries(AESKW)) {
126+
for (const [algo, paddings] of Object.entries(algos as any)) {
127+
for (const [padding, keySizes] of Object.entries(paddings as any)) {
128+
for (const [keySize, libraries] of Object.entries(keySizes as any)) {
129+
const name = `${type}/${algo}/${padding}/${keySize}`;
130+
if (crossFilter && !name.includes(crossFilter)) continue;
131+
const allowEnc =
132+
algo === 'aeskw'
133+
? (len: number) => !!len && len !== 8 && len % 8 === 0
134+
: (len: number) => !!len;
135+
encrypted[name] = await crossValidateAesKw(name, BUFFERS, libraries as any, allowEnc);
136+
if (process.env.CROSS_WAIT) await WP.waitOnline();
137+
}
138+
}
139+
}
140+
}
141+
await WP.waitOnline();
142+
console.log('Libraries cross-validated against each other correctly');
143+
144+
// Example: JSBT_BENCHMARK_FILTER=1MB JSBT_BENCHMARK_DIMENSIONS='buffer,type,algorithm,padding,key size,direction,library' node aeskw.ts
145+
await compare('AES-KW', { buffer: BUFFERS }, AESKW as any, {
146+
libraryDimensions: ['type', 'algorithm', 'padding', 'key size', 'library', 'direction'],
147+
defaults: {
148+
buffer: '64B',
149+
type: 'Wrap',
150+
library: 'noble',
151+
direction: 'encrypt',
152+
padding: 'Padding',
153+
algorithm: 'aeskwp',
154+
'key size': '256',
155+
},
156+
patchArgs: (args, obj) => {
157+
if (obj.direction === 'decrypt') {
158+
const buf =
159+
encrypted[`${obj.type}/${obj.algorithm}/${obj.padding}/${obj['key size']}`][obj.buffer];
160+
return [buf, args[1]];
161+
}
162+
return args;
163+
},
164+
bytes: ({ args }) => args[0].length,
165+
});
166+
}
167+
168+
import url from 'node:url';
169+
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
170+
main();
171+
}

0 commit comments

Comments
 (0)