diff --git a/benchmark/assert/partial-deep-strict-equal.js b/benchmark/assert/partial-deep-strict-equal.js new file mode 100644 index 00000000000000..7584d3a5beb5a1 --- /dev/null +++ b/benchmark/assert/partial-deep-strict-equal.js @@ -0,0 +1,148 @@ +'use strict'; + +const common = require('../common.js'); +const assert = require('assert'); + +const bench = common.createBenchmark(main, { + n: [10, 50, 200], + size: [1e3], + datasetName: [ + 'objects', + 'sets', + 'maps', + 'circularRefs', + 'typedArrays', + 'arrayBuffers', + 'dataViewArrayBuffers', + ], +}); + +function createObjects(length, depth = 0) { + return Array.from({ length }, () => ({ + foo: 'yarp', + nope: { + bar: '123', + a: [1, 2, 3], + c: {}, + b: !depth ? createObjects(2, depth + 1) : [], + }, + })); +} + +function createSets(length, depth = 0) { + return Array.from({ length }, () => new Set([ + 'yarp', + '123', + 1, + 2, + 3, + null, + { + simple: 'object', + number: 42, + }, + ['array', 'with', 'values'], + !depth ? new Set([1, 2, { nested: true }]) : new Set(), + !depth ? createSets(2, depth + 1) : null, + ])); +} + +function createMaps(length, depth = 0) { + return Array.from({ length }, () => new Map([ + ['primitiveKey', 'primitiveValue'], + [42, 'numberKey'], + ['objectValue', { a: 1, b: 2 }], + ['arrayValue', [1, 2, 3]], + ['nestedMap', new Map([['a', 1], ['b', { deep: true }]])], + [{ objectKey: true }, 'value from object key'], + [[1, 2, 3], 'value from array key'], + [!depth ? createMaps(2, depth + 1) : null, 'recursive value'], + ])); +} + +function createCircularRefs(length) { + return Array.from({ length }, () => { + const circularSet = new Set(); + const circularMap = new Map(); + const circularObj = { name: 'circular object' }; + + circularSet.add('some value'); + circularSet.add(circularSet); + + circularMap.set('self', circularMap); + circularMap.set('value', 'regular value'); + + circularObj.self = circularObj; + + const objA = { name: 'A' }; + const objB = { name: 'B' }; + objA.ref = objB; + objB.ref = objA; + + circularSet.add(objA); + circularMap.set('objB', objB); + + return { + circularSet, + circularMap, + circularObj, + objA, + objB, + }; + }); +} + +function createTypedArrays(length) { + return Array.from({ length }, () => { + const buffer = new ArrayBuffer(32); + + return { + int8: new Int8Array(buffer, 0, 4), + uint8: new Uint8Array(buffer, 4, 4), + uint8Clamped: new Uint8ClampedArray(buffer, 8, 4), + int16: new Int16Array([1, 2, 3]), + uint16: new Uint16Array([1, 2, 3]), + int32: new Int32Array([1, 2, 3]), + uint32: new Uint32Array([1, 2, 3]), + float32: new Float32Array([1.1, 2.2, 3.3]), + float64: new Float64Array([1.1, 2.2, 3.3]), + bigInt64: new BigInt64Array([1n, 2n, 3n]), + bigUint64: new BigUint64Array([1n, 2n, 3n]), + }; + }); +} + +function createArrayBuffers(length) { + return Array.from({ length }, (_, n) => new ArrayBuffer(n)); +} + +function createDataViewArrayBuffers(length) { + return Array.from({ length }, (_, n) => new DataView(new ArrayBuffer(n))); +} + +const datasetMappings = { + objects: createObjects, + sets: createSets, + maps: createMaps, + circularRefs: createCircularRefs, + typedArrays: createTypedArrays, + arrayBuffers: createArrayBuffers, + dataViewArrayBuffers: createDataViewArrayBuffers, +}; + +function getDatasets(datasetName, size) { + return { + actual: datasetMappings[datasetName](size), + expected: datasetMappings[datasetName](size), + }; +} + +function main({ size, n, datasetName }) { + const { actual, expected } = getDatasets(datasetName, size); + + bench.start(); + for (let i = 0; i < n; ++i) { + assert.partialDeepStrictEqual(actual, expected); + } + bench.end(n); +} diff --git a/lib/assert.js b/lib/assert.js index 16c06593601eac..318790980dfd0a 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -23,7 +23,6 @@ const { ArrayBufferIsView, ArrayBufferPrototypeGetByteLength, - ArrayFrom, ArrayIsArray, ArrayPrototypeIndexOf, ArrayPrototypeJoin, @@ -395,12 +394,11 @@ function partiallyCompareMaps(actual, expected, comparedObjects) { const expectedIterator = FunctionPrototypeCall(SafeMap.prototype[SymbolIterator], expected); for (const { 0: key, 1: expectedValue } of expectedIterator) { - if (!MapPrototypeHas(actual, key)) { + const actualValue = MapPrototypeGet(actual, key); + if (actualValue === undefined && !MapPrototypeHas(actual, key)) { return false; } - const actualValue = MapPrototypeGet(actual, key); - if (!compareBranch(actualValue, expectedValue, comparedObjects)) { return false; } @@ -474,28 +472,71 @@ function partiallyCompareArrayBuffersOrViews(actual, expected) { return true; } +// Adapted version of the "setEquiv" function in lib/internal/util/comparisons.js function partiallyCompareSets(actual, expected, comparedObjects) { if (SetPrototypeGetSize(expected) > SetPrototypeGetSize(actual)) { - return false; // `expected` can't be a subset if it has more elements + return false; } if (isDeepEqual === undefined) lazyLoadComparison(); + let set = null; - const actualArray = ArrayFrom(FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual)); - const expectedIterator = FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], expected); - const usedIndices = new SafeSet(); + // First, check if elements from expected exist in actual + for (const val of expected) { + // Fast path: direct inclusion check for both primitives and reference equality + if (actual.has(val)) { + continue; + } + + // For primitives, if not found directly, return false immediately + if (typeof val !== 'object' || val === null) { + return false; + } - expectedIteration: for (const expectedItem of expectedIterator) { - for (let actualIdx = 0; actualIdx < actualArray.length; actualIdx++) { - if (!usedIndices.has(actualIdx) && isDeepStrictEqual(actualArray[actualIdx], expectedItem)) { - usedIndices.add(actualIdx); - continue expectedIteration; + if (set === null) { + // Special case to avoid set creation for single-element comparison + if (SetPrototypeGetSize(expected) === 1) { + // Try to find any deep-equal object in actual + for (const actualItem of actual) { + if (!(typeof actualItem !== 'object' || actualItem === null) && isDeepStrictEqual(actualItem, val)) { + return true; + } + } + return false; } + set = new SafeSet(); } - return false; + + // Add this object for later deep comparison + set.add(val); } - return true; + // If all items were found directly, we're done + if (set === null) { + return true; + } + + // For remaining objects that need deep comparison + for (const actualItem of actual) { + // Only consider non-primitive values for deep comparison + if (!(typeof actualItem !== 'object' || actualItem === null)) { + // Check if this actual item deep-equals any remaining expected item + for (const expectedItem of set) { + if (isDeepStrictEqual(actualItem, expectedItem)) { + // Remove the matched item so we don't match it again + set.delete(expectedItem); + // If all items are matched, we can return early + if (set.size === 0) { + return true; + } + break; + } + } + } + } + + // If all objects in expected found matches, set will be empty + return set.size === 0; } const minusZeroSymbol = Symbol('-0'); @@ -510,21 +551,26 @@ function getZeroKey(item) { } function partiallyCompareArrays(actual, expected, comparedObjects) { + if (actual === expected) return true; + if (expected.length > actual.length) { return false; } + if (expected.length === 0) { + return true; + } + if (isDeepEqual === undefined) lazyLoadComparison(); // Create a map to count occurrences of each element in the expected array const expectedCounts = new SafeMap(); - const safeExpected = new SafeArrayIterator(expected); - for (const expectedItem of safeExpected) { - // Check if the item is a zero or a -0, as these need to be handled separately + const expectedIterator = new SafeArrayIterator(expected); + for (const expectedItem of expectedIterator) { if (expectedItem === 0) { const zeroKey = getZeroKey(expectedItem); - expectedCounts.set(zeroKey, (expectedCounts.get(zeroKey)?.count || 0) + 1); + expectedCounts.set(zeroKey, (expectedCounts.get(zeroKey) ?? 0) + 1); } else { let found = false; for (const { 0: key, 1: count } of expectedCounts) { @@ -540,10 +586,8 @@ function partiallyCompareArrays(actual, expected, comparedObjects) { } } - const safeActual = new SafeArrayIterator(actual); - - for (const actualItem of safeActual) { - // Check if the item is a zero or a -0, as these need to be handled separately + const actualIterator = new SafeArrayIterator(actual); + for (const actualItem of actualIterator) { if (actualItem === 0) { const zeroKey = getZeroKey(actualItem); @@ -567,6 +611,10 @@ function partiallyCompareArrays(actual, expected, comparedObjects) { } } } + + if (expectedCounts.size === 0) { + return true; + } } return expectedCounts.size === 0;