diff --git a/benchmark/assert/partial-deep-strict-equal.js b/benchmark/assert/partial-deep-strict-equal.js new file mode 100644 index 00000000000000..028d5746ecbaa1 --- /dev/null +++ b/benchmark/assert/partial-deep-strict-equal.js @@ -0,0 +1,89 @@ +'use strict'; + +const common = require('../common.js'); +const assert = require('assert'); + +const bench = common.createBenchmark(main, { + n: [25, 2e2], + size: [1e2, 1e4], + datasetName: ['objects', 'sets', 'maps', 'arrayBuffers'], +}, { + combinationFilter: (p) => { + return p.size === 1e4 && p.n === 25 || + p.size === 1e3 && p.n === 2e2 || + p.size === 1e2 && p.n === 2e3 || + p.size === 1; + }, +}); + +function createObjects(length, depth = 0) { + return Array.from({ length }, (n) => ({ + foo: 'yarp', + nope: { + bar: '123', + a: [1, 2, 3], + baz: n, + c: {}, + b: !depth ? createObjects(2, depth + 1) : [], + }, + })); +} + +function createSets(length, depth = 0) { + return Array.from({ length }, (n) => new Set([ + 'yarp', + { + bar: '123', + a: [1, 2, 3], + baz: n, + c: {}, + b: !depth ? createSets(2, depth + 1) : new Set(), + }, + ])); +} + +function createMaps(length, depth = 0) { + return Array.from({ length }, (n) => new Map([ + ['foo', 'yarp'], + ['nope', new Map([ + ['bar', '123'], + ['a', [1, 2, 3]], + ['baz', n], + ['c', {}], + ['b', !depth ? createMaps(2, depth + 1) : new Map()], + ])], + ])); +} + +function createArrayBuffers(length) { + return Array.from({ length }, (n) => { + if (n % 2) { + return new DataView(new ArrayBuffer(n)); + } + return new ArrayBuffer(n); + }); +} + +const datasetMappings = { + objects: createObjects, + sets: createSets, + maps: createMaps, + arrayBuffers: createArrayBuffers, +}; + +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..4d6629a8f5965b 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; } @@ -481,18 +479,38 @@ function partiallyCompareSets(actual, expected, comparedObjects) { if (isDeepEqual === undefined) lazyLoadComparison(); - const actualArray = ArrayFrom(FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual)); + // Create a map for faster lookups + const actualMap = new SafeMap(); + const actualIterator = FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual); const expectedIterator = FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], expected); - const usedIndices = new SafeSet(); - 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; + for (const actualItem of actualIterator) { + actualMap.set(actualItem, true); + } + + for (const expectedItem of expectedIterator) { + let foundMatch = false; + + // Check for primitives first to avoid useless loops + if (isPrimitive(expectedItem)) { + if (actualMap.has(expectedItem)) { + actualMap.delete(expectedItem); + foundMatch = true; + } + } else { + // Check for non-primitives + for (const { 0: actualItem } of actualMap) { + if (isDeepStrictEqual(actualItem, expectedItem)) { + actualMap.delete(actualItem); + foundMatch = true; + break; + } } } - return false; + + if (!foundMatch) { + return false; + } } return true; @@ -518,13 +536,11 @@ function partiallyCompareArrays(actual, expected, comparedObjects) { // 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 + for (const expectedItem of new SafeArrayIterator(expected)) { 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 +556,7 @@ 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 + for (const actualItem of new SafeArrayIterator(actual)) { if (actualItem === 0) { const zeroKey = getZeroKey(actualItem); @@ -723,6 +736,10 @@ function compareExceptionKey(actual, expected, key, message, keys, fn) { } } +function isPrimitive(value) { + return typeof value !== 'object' || value === null; +} + function expectedException(actual, expected, message, fn) { let generatedMessage = false; let throwError = false; @@ -741,7 +758,7 @@ function expectedException(actual, expected, message, fn) { } throwError = true; // Handle primitives properly. - } else if (typeof actual !== 'object' || actual === null) { + } else if (isPrimitive(actual)) { const err = new AssertionError({ actual, expected,