Skip to content

Commit c9575e1

Browse files
authored
feat(core/checksum): add crc32 and sha256 impls (#2101)
* feat(core/checksum): add crc32 and sha256 impls * feat(core/checksum): refactor md5
1 parent 2dcefdb commit c9575e1

56 files changed

Lines changed: 1849 additions & 650 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/breezy-spies-heal.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@smithy/fetch-http-handler": minor
3+
"@smithy/node-http-handler": minor
4+
---
5+
6+
use streamCollector from core/serde

.changeset/tricky-baboons-help.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@smithy/core": minor
3+
---
4+
5+
add crc32 and sha256 checksum impls

api-snapshot/api.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,18 @@
8787
"@smithy/core/checksum": {
8888
"blobHasher": "function",
8989
"blobReader": "function",
90+
"Crc32": "function",
91+
"Crc32Js": "function",
92+
"Crc32Node": "function(node-only)",
9093
"fileStreamHasher": "function(node-only)",
9194
"Md5": "function",
92-
"readableStreamHasher": "function(node-only)"
95+
"Md5Js": "function",
96+
"Md5Node": "function(node-only)",
97+
"readableStreamHasher": "function(node-only)",
98+
"Sha256": "function",
99+
"Sha256Js": "function",
100+
"Sha256Node": "function(node-only)",
101+
"Sha256WebCrypto": "function"
93102
},
94103
"@smithy/core/client": {
95104
"_json": "function",
@@ -458,6 +467,7 @@
458467
"calculateBodyLength": "function",
459468
"ChecksumStream": "function",
460469
"ChecksumStreamInit": "type(interface)",
470+
"concatBytes": "function",
461471
"copyDocumentWithTransform": "function",
462472
"createBufferedReadable": "function",
463473
"createChecksumStream": "function",
@@ -510,6 +520,7 @@
510520
"splitEvery": "function",
511521
"splitHeader": "function",
512522
"splitStream": "function",
523+
"streamCollector": "function",
513524
"strictParseByte": "function",
514525
"strictParseDouble": "function",
515526
"strictParseFloat": "function",

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
"premove": "4.0.0"
3737
},
3838
"devDependencies": {
39+
"@aws-crypto/crc32": "5.2.0",
40+
"@aws-crypto/sha256-js": "5.2.0",
3941
"@ianvs/prettier-plugin-sort-imports": "^4.7.1",
4042
"@microsoft/api-extractor": "7.52.7",
4143
"@rollup/plugin-json": "^6.1.0",

packages/core/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
"name": "@smithy/core",
33
"version": "3.26.0",
44
"scripts": {
5+
"benchmark:cbor": "node ./scripts/cbor-perf.mjs",
6+
"benchmark:checksum": "node ./scripts/checksum-perf.mjs",
57
"build": "concurrently 'yarn:build:types' 'yarn:build:es:cjs'",
68
"build:es:cjs": "premove dist-es && yarn g:tsc -p tsconfig.es.json && node ../../scripts/inline",
79
"build:types": "premove dist-types && yarn g:tsc -p tsconfig.types.json",
@@ -15,8 +17,7 @@
1517
"test": "yarn g:vitest run",
1618
"test:watch": "yarn g:vitest watch",
1719
"test:integration": "yarn g:vitest run -c vitest.config.integ.mts",
18-
"test:integration:watch": "yarn g:vitest watch -c vitest.config.integ.mts",
19-
"test:cbor:perf": "node ./scripts/cbor-perf.mjs"
20+
"test:integration:watch": "yarn g:vitest watch -c vitest.config.integ.mts"
2021
},
2122
"main": "./dist-cjs/index.js",
2223
"module": "./dist-es/index.js",
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Checksum Benchmarks
2+
3+
Platform: Node.js v22.22.2 (linux x64)
4+
5+
Date: 2026-06-24T17:04:04.733Z
6+
7+
Iterations per size: [10000, 5000, 2000, 200, 20, 5, 3], Warmup: 1s per algo
8+
9+
## CRC-32
10+
11+
| Size | Crc32Js (JS) | Crc32Node (node:zlib) | @aws-crypto/crc32 |
12+
| ---- | ------------ | --------------------- | ----------------- |
13+
| 32B | 35.3 MB/s | 34.4 MB/s | 28.8 MB/s |
14+
| 256B | 96.0 MB/s | 307.9 MB/s | 77.9 MB/s |
15+
| 1KB | 116.1 MB/s | 1.31 GB/s | 90.4 MB/s |
16+
| 64KB | 121.0 MB/s | 4.52 GB/s | 101.4 MB/s |
17+
| 1MB | 121.2 MB/s | 4.76 GB/s | 100.8 MB/s |
18+
| 10MB | 121.2 MB/s | 4.76 GB/s | 100.3 MB/s |
19+
| 50MB | 121.2 MB/s | 4.74 GB/s | 100.9 MB/s |
20+
21+
## SHA-256 (hash)
22+
23+
| Size | Sha256Js (JS) | Sha256Node (node:crypto) | @aws-crypto/sha256-js |
24+
| ---- | ------------- | ------------------------ | --------------------- |
25+
| 32B | 9.4 MB/s | 11.5 MB/s | 5.2 MB/s |
26+
| 256B | 49.0 MB/s | 71.8 MB/s | 35.0 MB/s |
27+
| 1KB | 92.4 MB/s | 211.5 MB/s | 69.8 MB/s |
28+
| 64KB | 116.4 MB/s | 1.58 GB/s | 99.9 MB/s |
29+
| 1MB | 118.8 MB/s | 1.69 GB/s | 100.4 MB/s |
30+
| 10MB | 118.9 MB/s | 1.70 GB/s | 100.6 MB/s |
31+
| 50MB | 118.7 MB/s | 1.70 GB/s | 100.6 MB/s |
32+
33+
## SHA-256 (HMAC)
34+
35+
| Size | Sha256Js (JS) | Sha256Node (node:crypto) | @aws-crypto/sha256-js |
36+
| ---- | ------------- | ------------------------ | --------------------- |
37+
| 32B | 3.0 MB/s | 10.6 MB/s | 2.7 MB/s |
38+
| 256B | 20.2 MB/s | 76.7 MB/s | 20.0 MB/s |
39+
| 1KB | 60.4 MB/s | 344.0 MB/s | 55.3 MB/s |
40+
| 64KB | 115.9 MB/s | 1.59 GB/s | 99.6 MB/s |
41+
| 1MB | 118.5 MB/s | 1.69 GB/s | 100.4 MB/s |
42+
| 10MB | 119.0 MB/s | 1.70 GB/s | 100.2 MB/s |
43+
| 50MB | 115.4 MB/s | 1.70 GB/s | 100.6 MB/s |
44+
45+
## MD5
46+
47+
Md5Js vs old @smithy/md5-js (unrolled rounds): 0.9x (32B), 2.2x (256B), 2.2x (1KB), 2.1x (64KB), 2.1x (1MB)
48+
49+
| Size | Md5Js (JS) | Md5Node (node:crypto) |
50+
| ---- | ---------- | --------------------- |
51+
| 32B | 10.9 MB/s | 13.3 MB/s |
52+
| 256B | 58.4 MB/s | 87.6 MB/s |
53+
| 1KB | 114.3 MB/s | 261.1 MB/s |
54+
| 64KB | 142.7 MB/s | 756.4 MB/s |
55+
| 1MB | 143.4 MB/s | 775.8 MB/s |
56+
| 10MB | 143.6 MB/s | 777.4 MB/s |
57+
| 50MB | 143.3 MB/s | 777.6 MB/s |
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { writeFileSync } from "node:fs";
2+
import { createRequire } from "node:module";
3+
import { dirname, resolve } from "node:path";
4+
import { performance } from "node:perf_hooks";
5+
import { fileURLToPath } from "node:url";
6+
7+
const __dirname = dirname(fileURLToPath(import.meta.url));
8+
const coreDir = resolve(__dirname, "..");
9+
const outputPath = resolve(coreDir, "planning", "checksums.md");
10+
11+
const require = createRequire(import.meta.url);
12+
const checksum = require(resolve(coreDir, "dist-cjs/submodules/checksum/index.js"));
13+
const { Crc32Js, Crc32Node, Sha256Js, Sha256Node, Md5Js, Md5Node } = checksum;
14+
const { AwsCrc32 } = require("@aws-crypto/crc32");
15+
const { Sha256: AwsSha256 } = require("@aws-crypto/sha256-js");
16+
17+
const SIZES = [32, 256, 1024, 64 * 1024, 1024 * 1024, 10 * 1024 * 1024, 50 * 1024 * 1024];
18+
const ITERATIONS = [10000, 5000, 2000, 200, 20, 5, 3 ];
19+
20+
function generateData(size) {
21+
const buf = new Uint8Array(size);
22+
for (let i = 0; i < size; ++i) {
23+
buf[i] = i & 0xff;
24+
}
25+
return buf;
26+
}
27+
28+
const WARMUP_DATA = generateData(64 * 1024);
29+
30+
/**
31+
* Runs algo on 64KB data for 1 second to ensure V8 optimization.
32+
*/
33+
async function warmup(Impl, ...ctorArgs) {
34+
const deadline = performance.now() + 1000;
35+
while (performance.now() < deadline) {
36+
const h = new Impl(...ctorArgs);
37+
h.update(WARMUP_DATA);
38+
await h.digest();
39+
}
40+
}
41+
42+
async function bench(Impl, data, iterations, ...ctorArgs) {
43+
const start = performance.now();
44+
for (let i = 0; i < iterations; ++i) {
45+
const h = new Impl(...ctorArgs);
46+
h.update(data);
47+
await h.digest();
48+
}
49+
return performance.now() - start;
50+
}
51+
52+
function formatSize(bytes) {
53+
if (bytes >= 1024 * 1024) return `${bytes / (1024 * 1024)}MB`;
54+
if (bytes >= 1024) return `${bytes / 1024}KB`;
55+
return `${bytes}B`;
56+
}
57+
58+
function formatThroughput(bytesPerSec) {
59+
if (bytesPerSec >= 1024 * 1024 * 1024) return `${(bytesPerSec / (1024 * 1024 * 1024)).toFixed(2)} GB/s`;
60+
if (bytesPerSec >= 1024 * 1024) return `${(bytesPerSec / (1024 * 1024)).toFixed(1)} MB/s`;
61+
return `${(bytesPerSec / 1024).toFixed(1)} KB/s`;
62+
}
63+
64+
function alignedTable(headers, rows) {
65+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
66+
const pad = (s, i) => s.padEnd(widths[i]);
67+
const sep = widths.map((w) => "-".repeat(w));
68+
const out = [];
69+
out.push("| " + headers.map(pad).join(" | ") + " |");
70+
out.push("| " + sep.join(" | ") + " |");
71+
for (const row of rows) {
72+
out.push("| " + row.map(pad).join(" | ") + " |");
73+
}
74+
return out;
75+
}
76+
77+
async function runSection(label, impls, extraCtorArgs) {
78+
const rows = [];
79+
for (let si = 0; si < SIZES.length; ++si) {
80+
const size = SIZES[si];
81+
const data = generateData(size);
82+
const iters = ITERATIONS[si];
83+
const row = [formatSize(size)];
84+
const parts = [];
85+
for (const { name, Impl, ctorArgs } of impls) {
86+
const ms = await bench(Impl, data, iters, ...(ctorArgs || []));
87+
const tp = formatThroughput((size * iters * 1000) / ms);
88+
row.push(tp);
89+
parts.push(`${name}=${tp}`);
90+
}
91+
rows.push(row);
92+
console.log(` ${label} ${formatSize(size)}: ${parts.join(", ")}`);
93+
}
94+
return rows;
95+
}
96+
97+
// --- Benchmark definitions ---
98+
99+
const sections = [
100+
{
101+
title: "CRC-32",
102+
headers: ["Size", "Crc32Js (JS)", "Crc32Node (node:zlib)", "@aws-crypto/crc32"],
103+
impls: [
104+
{ name: "Crc32Js", Impl: Crc32Js },
105+
{ name: "Crc32Node", Impl: Crc32Node },
106+
{ name: "@aws-crypto/crc32", Impl: AwsCrc32 },
107+
],
108+
},
109+
{
110+
title: "SHA-256 (hash)",
111+
headers: ["Size", "Sha256Js (JS)", "Sha256Node (node:crypto)", "@aws-crypto/sha256-js"],
112+
impls: [
113+
{ name: "Sha256Js", Impl: Sha256Js },
114+
{ name: "Sha256Node", Impl: Sha256Node },
115+
{ name: "@aws-crypto/sha256-js", Impl: AwsSha256 },
116+
],
117+
},
118+
{
119+
title: "SHA-256 (HMAC)",
120+
headers: ["Size", "Sha256Js (JS)", "Sha256Node (node:crypto)", "@aws-crypto/sha256-js"],
121+
impls: [
122+
{ name: "Sha256Js", Impl: Sha256Js, ctorArgs: [generateData(32)] },
123+
{ name: "Sha256Node", Impl: Sha256Node, ctorArgs: [generateData(32)] },
124+
{ name: "@aws-crypto/sha256-js", Impl: AwsSha256, ctorArgs: [generateData(32)] },
125+
],
126+
},
127+
{
128+
title: "MD5",
129+
note: "Md5Js vs old @smithy/md5-js (unrolled rounds): 0.9x (32B), 2.2x (256B), 2.2x (1KB), 2.1x (64KB), 2.1x (1MB)",
130+
headers: ["Size", "Md5Js (JS)", "Md5Node (node:crypto)"],
131+
impls: [
132+
{ name: "Md5Js", Impl: Md5Js },
133+
{ name: "Md5Node", Impl: Md5Node },
134+
],
135+
},
136+
];
137+
138+
// --- Run ---
139+
140+
console.log("Warming up...");
141+
const allImpls = sections.flatMap((s) => s.impls);
142+
for (const { name, Impl, ctorArgs } of allImpls) {
143+
process.stdout.write(` ${name}...`);
144+
await warmup(Impl, ...(ctorArgs || []));
145+
console.log(" done");
146+
}
147+
await new Promise((r) => setTimeout(r, 1000));
148+
149+
console.log("Running checksum benchmarks...\n");
150+
151+
const lines = [
152+
"# Checksum Benchmarks\n",
153+
`Platform: Node.js ${process.version} (${process.platform} ${process.arch})\n`,
154+
`Date: ${new Date().toISOString()}\n`,
155+
`Iterations per size: [${ITERATIONS.join(", ")}], Warmup: 1s per algo\n`,
156+
];
157+
158+
for (const { title, headers, impls, note } of sections) {
159+
lines.push(`\n## ${title}\n`);
160+
if (note) {
161+
lines.push(`${note}\n`);
162+
}
163+
const rows = await runSection(title, impls);
164+
lines.push(...alignedTable(headers, rows));
165+
}
166+
167+
const md = lines.join("\n") + "\n";
168+
writeFileSync(outputPath, md);
169+
console.log(`\nResults written to ${outputPath}`);

0 commit comments

Comments
 (0)