assert,util: improve deep comparison performance

PR-URL: https://github.com/nodejs/node/pull/61076
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
This commit is contained in:
Ruben Bridgewater
2025-12-15 13:27:40 +01:00
committed by Node.js GitHub Bot
parent 416db75e42
commit a968e4e672
2 changed files with 89 additions and 71 deletions

View File

@@ -4,7 +4,6 @@ const {
Array,
ArrayBuffer,
ArrayIsArray,
ArrayPrototypeFilter,
ArrayPrototypePush,
BigInt,
BigInt64Array,
@@ -91,7 +90,7 @@ const wellKnownConstructors = new SafeSet()
.add(WeakMap)
.add(WeakSet);
if (Float16Array) { // TODO(BridgeAR): Remove when regularly supported
if (Float16Array) { // TODO(BridgeAR): Remove when Flag got removed from V8
wellKnownConstructors.add(Float16Array);
}
@@ -127,6 +126,11 @@ const {
getOwnNonIndexProperties,
} = internalBinding('util');
let kKeyObject;
let kExtractable;
let kAlgorithm;
let kKeyUsages;
const kStrict = 2;
const kStrictWithoutPrototypes = 3;
const kLoose = 0;
@@ -152,7 +156,7 @@ function isPartialUint8Array(a, b) {
}
let offsetA = 0;
for (let offsetB = 0; offsetB < lenB; offsetB++) {
while (!ObjectIs(a[offsetA], b[offsetB])) {
while (a[offsetA] !== b[offsetB]) {
offsetA++;
if (offsetA > lenA - lenB + offsetB) {
return false;
@@ -187,11 +191,7 @@ function areSimilarFloatArrays(a, b) {
}
function areSimilarTypedArrays(a, b) {
if (a.byteLength !== b.byteLength) {
return false;
}
return compare(new Uint8Array(a.buffer, a.byteOffset, a.byteLength),
new Uint8Array(b.buffer, b.byteOffset, b.byteLength)) === 0;
return a.byteLength === b.byteLength && compare(a, b) === 0;
}
function areEqualArrayBuffers(buf1, buf2) {
@@ -225,7 +225,7 @@ function isEqualBoxedPrimitive(val1, val2) {
assert.fail(`Unknown boxed type ${val1}`);
}
function isEnumerableOrIdentical(val1, val2, prop, mode, memos, method) {
function isEnumerableOrIdentical(val1, val2, prop, mode, memos) {
return hasEnumerable(val2, prop) || // This is handled by Object.keys()
(mode === kPartial && (val2[prop] === undefined || (prop === 'message' && val2[prop] === ''))) ||
innerDeepEqual(val1[prop], val2[prop], mode, memos);
@@ -400,8 +400,10 @@ function objectComparisonStart(val1, val2, mode, memos) {
return false;
}
} else if (isCryptoKey(val1)) {
const { kKeyObject } = require('internal/crypto/util');
const { kExtractable, kAlgorithm, kKeyUsages } = require('internal/crypto/keys');
if (kKeyObject === undefined) {
kKeyObject = require('internal/crypto/util').kKeyObject;
({ kExtractable, kAlgorithm, kKeyUsages } = require('internal/crypto/keys'));
}
if (!isCryptoKey(val2) ||
val1[kExtractable] !== val2[kExtractable] ||
!innerDeepEqual(val1[kAlgorithm], val2[kAlgorithm], mode, memos) ||
@@ -417,18 +419,11 @@ function objectComparisonStart(val1, val2, mode, memos) {
return keyCheck(val1, val2, mode, memos, kNoIterator);
}
function getEnumerables(val, keys) {
return ArrayPrototypeFilter(keys, (key) => hasEnumerable(val, key));
}
function partialSymbolEquiv(val1, val2, keys2) {
const symbolKeys = getOwnSymbols(val2);
if (symbolKeys.length !== 0) {
for (const key of symbolKeys) {
if (hasEnumerable(val2, key)) {
if (!hasEnumerable(val1, key)) {
return false;
}
ArrayPrototypePush(keys2, key);
}
}
@@ -460,32 +455,19 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) {
} else if (keys2.length !== (keys1 = ObjectKeys(val1)).length) {
return false;
} else if (mode === kStrict || mode === kStrictWithoutPrototypes) {
const symbolKeysA = getOwnSymbols(val1);
if (symbolKeysA.length !== 0) {
let count = 0;
for (const key of symbolKeysA) {
if (hasEnumerable(val1, key)) {
if (!hasEnumerable(val2, key)) {
return false;
}
ArrayPrototypePush(keys2, key);
count++;
} else if (hasEnumerable(val2, key)) {
return false;
}
for (const key of getOwnSymbols(val1)) {
if (hasEnumerable(val1, key)) {
ArrayPrototypePush(keys1, key);
}
const symbolKeysB = getOwnSymbols(val2);
if (symbolKeysA.length !== symbolKeysB.length &&
getEnumerables(val2, symbolKeysB).length !== count) {
return false;
}
} else {
const symbolKeysB = getOwnSymbols(val2);
if (symbolKeysB.length !== 0 &&
getEnumerables(val2, symbolKeysB).length !== 0) {
return false;
}
for (const key of getOwnSymbols(val2)) {
if (hasEnumerable(val2, key)) {
ArrayPrototypePush(keys2, key);
}
}
if (keys1.length !== keys2.length) {
return false;
}
}
}
@@ -647,16 +629,14 @@ function partialObjectSetEquiv(array, a, b, mode, memo) {
}
function arrayHasEqualElement(array, val1, mode, memo, comparator, start, end) {
let matched = false;
for (let i = end - 1; i >= start; i--) {
if (comparator(val1, array[i], mode, memo)) {
// Remove the matching element to make sure we do not check that again.
array.splice(i, 1);
matched = true;
break;
// Move the matching element to make sure we do not check that again.
array[i] = array[end];
return true;
}
}
return matched;
return false;
}
function setObjectEquiv(array, a, b, mode, memo) {
@@ -798,18 +778,16 @@ function partialObjectMapEquiv(array, a, b, mode, memo) {
}
function arrayHasEqualMapElement(array, key1, item1, b, mode, memo, comparator, start, end) {
let matched = false;
for (let i = end - 1; i >= start; i--) {
const key2 = array[i];
if (comparator(key1, key2, mode, memo) &&
innerDeepEqual(item1, b.get(key2), mode, memo)) {
// Remove the matching element to make sure we do not check that again.
array.splice(i, 1);
matched = true;
break;
// Move the matching element to make sure we do not check that again.
array[i] = array[end];
return true;
}
}
return matched;
return false;
}
function mapObjectEquiv(array, a, b, mode, memo) {
@@ -898,17 +876,21 @@ function mapEquiv(a, b, mode, memo) {
}
function partialSparseArrayEquiv(a, b, mode, memos, startA, startB) {
let aPos = 0;
const keysA = ObjectKeys(a).slice(startA);
const keysB = ObjectKeys(b).slice(startB);
if (keysA.length < keysB.length) {
let aPos = startA;
const keysA = ObjectKeys(a);
const keysB = ObjectKeys(b);
const keysBLength = keysB.length;
const keysALength = keysA.length;
const lenA = keysALength - startA;
const lenB = keysBLength - startB;
if (lenA < lenB) {
return false;
}
for (let i = 0; i < keysB.length; i++) {
const keyB = keysB[i];
for (let i = 0; i < lenB; i++) {
const keyB = keysB[startB + i];
while (!innerDeepEqual(a[keysA[aPos]], b[keyB], mode, memos)) {
aPos++;
if (aPos > keysA.length - keysB.length + i) {
if (aPos > keysALength - lenB + i) {
return false;
}
}
@@ -979,8 +961,11 @@ function objEquiv(a, b, mode, keys1, keys2, memos, iterationType) {
// property in V8 13.0 compared to calling Object.propertyIsEnumerable()
// and accessing the property regularly.
const descriptor = ObjectGetOwnPropertyDescriptor(a, key);
if (!descriptor?.enumerable ||
!innerDeepEqual(descriptor.value !== undefined ? descriptor.value : a[key], b[key], mode, memos)) {
if (descriptor === undefined || descriptor.enumerable !== true) {
return false;
}
const value = descriptor.writable !== undefined ? descriptor.value : a[key];
if (!innerDeepEqual(value, b[key], mode, memos)) {
return false;
}
}
@@ -1029,10 +1014,7 @@ module.exports = {
return detectCycles(val1, val2, kLoose);
},
isDeepStrictEqual(val1, val2, skipPrototype) {
if (skipPrototype) {
return detectCycles(val1, val2, kStrictWithoutPrototypes);
}
return detectCycles(val1, val2, kStrict);
return detectCycles(val1, val2, skipPrototype ? kStrictWithoutPrototypes : kStrict);
},
isPartialStrictEqual(val1, val2) {
return detectCycles(val1, val2, kPartial);