Skip to content

Commit 230a7fc

Browse files
panvaaduh95
authored andcommitted
lib: optimize internal Web IDL dictionaries
Flatten dictionary members at construction while preserving inheritance order. Skip member scans for nullish inputs without defaults or required members, and define own descriptor properties to avoid inherited setters. Signed-off-by: Filip Skokan <panva.ip@gmail.com> PR-URL: #65857 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent 55db777 commit 230a7fc

2 files changed

Lines changed: 108 additions & 44 deletions

File tree

‎lib/internal/webidl.js‎

Lines changed: 41 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -698,7 +698,7 @@ function createDictionaryConverter(
698698
};
699699

700700
const dictionaries = ArrayIsArray(members[0]) ? members : [members];
701-
const sortedDictionaries = [];
701+
const sortedMembers = [];
702702

703703
function ownMember(member, key) {
704704
return ObjectPrototypeHasOwnProperty(member, key) ? member[key] : undefined;
@@ -712,7 +712,7 @@ function createDictionaryConverter(
712712
// from least-derived to most-derived and sort only within each dictionary.
713713
// Callers with inheritance pass one member array per dictionary level.
714714
for (let i = 0; i < dictionaries.length; i++) {
715-
const sortedMembers = ArrayPrototypeToSorted(
715+
const sortedDictionary = ArrayPrototypeToSorted(
716716
dictionaries[i],
717717
compareMembers,
718718
);
@@ -721,22 +721,21 @@ function createDictionaryConverter(
721721
// Re-materialize each descriptor once with every key present, copied from
722722
// own properties only. The ordinary prototype is deliberate: nothing
723723
// consults it now, and detaching it measurably slows these reads down.
724-
for (let j = 0; j < sortedMembers.length; j++) {
725-
const member = sortedMembers[j];
724+
for (let j = 0; j < sortedDictionary.length; j++) {
725+
const member = sortedDictionary[j];
726726
const defaultValue = ownMember(member, 'defaultValue');
727727
const required = ownMember(member, 'required');
728728
if (typeof defaultValue === 'function' || required) {
729729
anyMissingMemberHandling = true;
730730
}
731-
sortedMembers[j] = {
731+
setOwnProperty(sortedMembers, sortedMembers.length, {
732732
key: ownMember(member, 'key'),
733733
converter: ownMember(member, 'converter'),
734734
defaultValue,
735735
required,
736736
validator: ownMember(member, 'validator'),
737-
};
737+
});
738738
}
739-
ArrayPrototypePush(sortedDictionaries, sortedMembers);
740739
}
741740

742741
const hasMissingMemberHandling = anyMissingMemberHandling;
@@ -752,43 +751,41 @@ function createDictionaryConverter(
752751

753752
// Step 2: create the IDL dictionary value.
754753
const idlDict = { __proto__: null };
755-
756-
// Steps 3-4: iterate each dictionary level, then its sorted members.
757-
for (let i = 0; i < sortedDictionaries.length; i++) {
758-
const sortedMembers = sortedDictionaries[i];
759-
for (let j = 0; j < sortedMembers.length; j++) {
760-
const member = sortedMembers[j];
761-
// Step 4.1.1: get the dictionary member identifier.
762-
const key = member.key;
763-
// Steps 4.1.2-4.1.3: read the JavaScript member value.
764-
const jsMemberValue = jsDict == null ? undefined : jsDict[key];
765-
766-
// Step 4.1.4: convert and store present member values.
767-
if (jsMemberValue !== undefined) {
768-
const converter = member.converter;
769-
// Step 4.1.4.1: convert the JavaScript value to IDL.
770-
const idlMemberValue = converter(
771-
jsMemberValue,
772-
makeOptions(options, dictionaryMemberContext(key, options)),
773-
);
774-
// Validators are a Node.js extension after conversion. They let
775-
// consumers reject known unsupported values while dictionary
776-
// conversion still has precise member context. Web Crypto uses this
777-
// so SubtleCrypto.supports() can make accurate decisions from
778-
// normalized dictionaries instead of probing by running operations.
779-
member.validator?.(idlMemberValue, jsDict);
780-
// Step 4.1.4.2: set idlDict[key] to the IDL value.
781-
idlDict[key] = idlMemberValue;
782-
} else if (hasMissingMemberHandling) {
783-
if (typeof member.defaultValue === 'function') {
784-
// Step 4.1.5: store the member default value.
785-
idlDict[key] = member.defaultValue();
786-
} else if (member.required) {
787-
// Step 4.1.6: required missing members throw.
788-
throw makeException(
789-
missingDictionaryMemberMessage(dictionaryName, key),
790-
makeOptions(options, options.context, 'ERR_MISSING_OPTION'));
791-
}
754+
if (jsDict == null && !hasMissingMemberHandling) return idlDict;
755+
756+
// Steps 3-4: members retain their per-dictionary order from construction.
757+
for (let i = 0; i < sortedMembers.length; i++) {
758+
const member = sortedMembers[i];
759+
// Step 4.1.1: get the dictionary member identifier.
760+
const key = member.key;
761+
// Steps 4.1.2-4.1.3: read the JavaScript member value.
762+
const jsMemberValue = jsDict == null ? undefined : jsDict[key];
763+
764+
// Step 4.1.4: convert and store present member values.
765+
if (jsMemberValue !== undefined) {
766+
const converter = member.converter;
767+
// Step 4.1.4.1: convert the JavaScript value to IDL.
768+
const idlMemberValue = converter(
769+
jsMemberValue,
770+
makeOptions(options, dictionaryMemberContext(key, options)),
771+
);
772+
// Validators are a Node.js extension after conversion. They let
773+
// consumers reject known unsupported values while dictionary
774+
// conversion still has precise member context. Web Crypto uses this
775+
// so SubtleCrypto.supports() can make accurate decisions from
776+
// normalized dictionaries instead of probing by running operations.
777+
member.validator?.(idlMemberValue, jsDict);
778+
// Step 4.1.4.2: set idlDict[key] to the IDL value.
779+
idlDict[key] = idlMemberValue;
780+
} else if (hasMissingMemberHandling) {
781+
if (typeof member.defaultValue === 'function') {
782+
// Step 4.1.5: store the member default value.
783+
idlDict[key] = member.defaultValue();
784+
} else if (member.required) {
785+
// Step 4.1.6: required missing members throw.
786+
throw makeException(
787+
missingDictionaryMemberMessage(dictionaryName, key),
788+
makeOptions(options, options.context, 'ERR_MISSING_OPTION'));
792789
}
793790
}
794791
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
'use strict';
2+
3+
// Flags: --expose-internals
4+
5+
const common = require('../common');
6+
const assert = require('node:assert/strict');
7+
const { createDictionaryConverter } = require('internal/webidl');
8+
9+
const groups = [
10+
[{ key: 'z', converter: String }, { key: 'b', converter: String }],
11+
[{ key: 'c', converter: String }, { key: 'a', converter: String }],
12+
];
13+
let convert;
14+
Object.defineProperty(Array.prototype, 2, {
15+
configurable: true,
16+
get: common.mustNotCall(),
17+
set: common.mustNotCall(),
18+
});
19+
try {
20+
convert = createDictionaryConverter('Derived', groups);
21+
} finally {
22+
delete Array.prototype[2];
23+
}
24+
25+
const first = convert(null);
26+
const second = convert(undefined, new Proxy({}, { get: common.mustNotCall() }));
27+
assert.deepStrictEqual(first, { __proto__: null });
28+
assert.deepStrictEqual(second, first);
29+
assert.notStrictEqual(first, second);
30+
31+
const trace = [];
32+
const input = {
33+
__proto__: { c: 'inherited' },
34+
b: {
35+
toString: common.mustCall(() => {
36+
trace.push('convert b');
37+
input.z = 'changed';
38+
return 'b';
39+
}),
40+
},
41+
z: 'original',
42+
a: 'a',
43+
};
44+
const result = convert(new Proxy(input, {
45+
get(target, key) { trace.push(key); return target[key]; },
46+
}));
47+
assert.deepStrictEqual(trace, ['b', 'convert b', 'z', 'a', 'c']);
48+
assert.deepStrictEqual(result, { __proto__: null, b: 'b', z: 'changed', a: 'a', c: 'inherited' });
49+
50+
trace.length = 0;
51+
convert(new Proxy({}, { get(target, key) { trace.push(key); } }));
52+
assert.deepStrictEqual(trace, ['b', 'z', 'a', 'c']);
53+
54+
const defaults = createDictionaryConverter('Defaults', [[{
55+
key: 'z', converter: common.mustNotCall(), defaultValue: common.mustCall(() => [], 2),
56+
}]]);
57+
assert.notStrictEqual(defaults(null).z, defaults(undefined).z);
58+
59+
const required = createDictionaryConverter('Required', [[{
60+
key: 'z', converter: common.mustNotCall(), defaultValue: common.mustCall(() => 'default', 2),
61+
}], [
62+
{ key: 'a', converter: common.mustNotCall(), required: true },
63+
{ key: 'b', converter: common.mustNotCall(), defaultValue: common.mustNotCall() },
64+
]]);
65+
for (const value of [null, undefined]) {
66+
assert.throws(() => required(value), { code: 'ERR_MISSING_OPTION' });
67+
}

0 commit comments

Comments
 (0)