Skip to content

Commit e46e0d1

Browse files
panvanodejs-github-bot
authored andcommitted
crypto: optimize normalizeAlgorithm dispatch hot path
Replace the O(n) case-insensitive algorithm-name scan with an O(1) SafeMap lookup. The map is pre-built at module init alongside kSupportedAlgorithms. Hoist the opts object literal used in normalizeAlgorithm to module level to avoid allocating identical { prefix, context } objects on every call. Pre-compute ObjectKeys() for simpleAlgorithmDictionaries entries at module init to avoid allocating a new keys array on every normalizeAlgorithm call. Signed-off-by: Filip Skokan <panva.ip@gmail.com> PR-URL: #62756 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
1 parent e80b5d2 commit e46e0d1

1 file changed

Lines changed: 57 additions & 49 deletions

File tree

lib/internal/crypto/util.js

Lines changed: 57 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const {
1616
ObjectKeys,
1717
ObjectPrototypeHasOwnProperty,
1818
PromiseWithResolvers,
19+
SafeMap,
1920
SafeSet,
2021
StringPrototypeToUpperCase,
2122
Symbol,
@@ -453,8 +454,11 @@ const experimentalAlgorithms = [
453454
];
454455

455456
// Transform the algorithm definitions into the operation-keyed structure
457+
// Also builds a parallel Map<UPPERCASED_NAME, canonicalName> per operation
458+
// for O(1) case-insensitive algorithm name lookup in normalizeAlgorithm.
456459
function createSupportedAlgorithms(algorithmDefs) {
457460
const result = {};
461+
const nameMap = {};
458462

459463
for (const { 0: algorithmName, 1: operations } of ObjectEntries(algorithmDefs)) {
460464
// Skip algorithms that are conditionally not supported
@@ -465,6 +469,8 @@ function createSupportedAlgorithms(algorithmDefs) {
465469

466470
for (const { 0: operation, 1: dict } of ObjectEntries(operations)) {
467471
result[operation] ||= {};
472+
nameMap[operation] ||= new SafeMap();
473+
nameMap[operation].set(StringPrototypeToUpperCase(algorithmName), algorithmName);
468474

469475
// Add experimental warnings for experimental algorithms
470476
if (ArrayPrototypeIncludes(experimentalAlgorithms, algorithmName)) {
@@ -482,10 +488,11 @@ function createSupportedAlgorithms(algorithmDefs) {
482488
}
483489
}
484490

485-
return result;
491+
return { algorithms: result, nameMap };
486492
}
487493

488-
const kSupportedAlgorithms = createSupportedAlgorithms(kAlgorithmDefinitions);
494+
const { algorithms: kSupportedAlgorithms, nameMap: kAlgorithmNameMap } =
495+
createSupportedAlgorithms(kAlgorithmDefinitions);
489496

490497
const simpleAlgorithmDictionaries = {
491498
AesCbcParams: { iv: 'BufferSource' },
@@ -527,6 +534,12 @@ const simpleAlgorithmDictionaries = {
527534
TurboShakeParams: {},
528535
};
529536

537+
// Pre-compute ObjectKeys() for each dictionary entry at module init
538+
// to avoid allocating a new keys array on every normalizeAlgorithm call.
539+
for (const { 0: name, 1: types } of ObjectEntries(simpleAlgorithmDictionaries)) {
540+
simpleAlgorithmDictionaries[name] = { keys: ObjectKeys(types), types };
541+
}
542+
530543
function validateMaxBufferLength(data, name, max = kMaxBufferLength) {
531544
if (data.byteLength > max) {
532545
throw lazyDOMException(
@@ -537,6 +550,14 @@ function validateMaxBufferLength(data, name, max = kMaxBufferLength) {
537550

538551
let webidl;
539552

553+
// Keep this as a regular object. The WebIDL converters read and spread these
554+
// options on the normalizeAlgorithm hot path, and a null-prototype object
555+
// measurably regresses benchmark/misc/webcrypto-webidl normalizeAlgorithm-*.
556+
const kNormalizeAlgorithmOpts = {
557+
prefix: 'Failed to normalize algorithm',
558+
context: 'passed algorithm',
559+
};
560+
540561
// https://w3c.github.io/webcrypto/#algorithm-normalization-normalize-an-algorithm
541562
// adapted for Node.js from Deno's implementation
542563
// https://github.com/denoland/deno/blob/v1.29.1/ext/crypto/00_crypto.js#L195
@@ -549,69 +570,56 @@ function normalizeAlgorithm(algorithm, op) {
549570
// 1.
550571
const registeredAlgorithms = kSupportedAlgorithms[op];
551572
// 2. 3.
552-
const initialAlg = webidl.converters.Algorithm(algorithm, {
553-
prefix: 'Failed to normalize algorithm',
554-
context: 'passed algorithm',
555-
});
573+
const initialAlg = webidl.converters.Algorithm(algorithm,
574+
kNormalizeAlgorithmOpts);
556575
// 4.
557576
let algName = initialAlg.name;
558577

559-
// 5.
560-
let desiredType;
561-
for (const key in registeredAlgorithms) {
562-
if (!ObjectPrototypeHasOwnProperty(registeredAlgorithms, key)) {
563-
continue;
564-
}
565-
if (
566-
StringPrototypeToUpperCase(key) === StringPrototypeToUpperCase(algName)
567-
) {
568-
algName = key;
569-
desiredType = registeredAlgorithms[key];
570-
}
571-
}
572-
if (desiredType === undefined)
578+
// 5. Case-insensitive lookup via pre-built Map (O(1) instead of O(n)).
579+
const canonicalName = kAlgorithmNameMap[op]?.get(
580+
StringPrototypeToUpperCase(algName));
581+
if (canonicalName === undefined)
573582
throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
574583

584+
algName = canonicalName;
585+
const desiredType = registeredAlgorithms[algName];
586+
575587
// Fast path everything below if the registered dictionary is null
576588
if (desiredType === null)
577589
return { name: algName };
578590

579591
// 6.
580592
const normalizedAlgorithm = webidl.converters[desiredType](
581593
{ __proto__: algorithm, name: algName },
582-
{
583-
prefix: 'Failed to normalize algorithm',
584-
context: 'passed algorithm',
585-
},
594+
kNormalizeAlgorithmOpts,
586595
);
587596
// 7.
588597
normalizedAlgorithm.name = algName;
589598

590-
// 9.
591-
const dict = simpleAlgorithmDictionaries[desiredType];
592-
// 10.
593-
const dictKeys = dict ? ObjectKeys(dict) : [];
594-
for (let i = 0; i < dictKeys.length; i++) {
595-
const member = dictKeys[i];
596-
if (!ObjectPrototypeHasOwnProperty(dict, member))
597-
continue;
598-
const idlType = dict[member];
599-
const idlValue = normalizedAlgorithm[member];
600-
// 3.
601-
if (idlType === 'BufferSource' && idlValue) {
602-
const isView = ArrayBufferIsView(idlValue);
603-
normalizedAlgorithm[member] = TypedArrayPrototypeSlice(
604-
new Uint8Array(
605-
isView ? getDataViewOrTypedArrayBuffer(idlValue) : idlValue,
606-
isView ? getDataViewOrTypedArrayByteOffset(idlValue) : 0,
607-
isView ? getDataViewOrTypedArrayByteLength(idlValue) : ArrayBufferPrototypeGetByteLength(idlValue),
608-
),
609-
);
610-
} else if (idlType === 'HashAlgorithmIdentifier') {
611-
normalizedAlgorithm[member] = normalizeAlgorithm(idlValue, 'digest');
612-
} else if (idlType === 'AlgorithmIdentifier') {
613-
// This extension point is not used by any supported algorithm (yet?)
614-
throw lazyDOMException('Not implemented.', 'NotSupportedError');
599+
// 9. 10. Pre-computed keys and types from simpleAlgorithmDictionaries.
600+
const dictMeta = simpleAlgorithmDictionaries[desiredType];
601+
if (dictMeta) {
602+
const { keys: dictKeys, types: dictTypes } = dictMeta;
603+
for (let i = 0; i < dictKeys.length; i++) {
604+
const member = dictKeys[i];
605+
const idlType = dictTypes[member];
606+
const idlValue = normalizedAlgorithm[member];
607+
// 3.
608+
if (idlType === 'BufferSource' && idlValue) {
609+
const isView = ArrayBufferIsView(idlValue);
610+
normalizedAlgorithm[member] = TypedArrayPrototypeSlice(
611+
new Uint8Array(
612+
isView ? getDataViewOrTypedArrayBuffer(idlValue) : idlValue,
613+
isView ? getDataViewOrTypedArrayByteOffset(idlValue) : 0,
614+
isView ? getDataViewOrTypedArrayByteLength(idlValue) : ArrayBufferPrototypeGetByteLength(idlValue),
615+
),
616+
);
617+
} else if (idlType === 'HashAlgorithmIdentifier') {
618+
normalizedAlgorithm[member] = normalizeAlgorithm(idlValue, 'digest');
619+
} else if (idlType === 'AlgorithmIdentifier') {
620+
// This extension point is not used by any supported algorithm (yet?)
621+
throw lazyDOMException('Not implemented.', 'NotSupportedError');
622+
}
615623
}
616624
}
617625

0 commit comments

Comments
 (0)