From e932899f0bb12d5fe42389ddde4bc814054d0588 Mon Sep 17 00:00:00 2001 From: Giovanni Date: Fri, 10 Jan 2025 16:29:55 +0100 Subject: [PATCH] assert: improve partialDeepStrictEqual performance and add benchmark --- benchmark/assert/partial-deep-strict-equal.js | 83 +++++++++++++++++++ lib/assert.js | 57 ++++++++----- 2 files changed, 118 insertions(+), 22 deletions(-) create mode 100644 benchmark/assert/partial-deep-strict-equal.js diff --git a/benchmark/assert/partial-deep-strict-equal.js b/benchmark/assert/partial-deep-strict-equal.js new file mode 100644 index 00000000000000..6d220b52b21f70 --- /dev/null +++ b/benchmark/assert/partial-deep-strict-equal.js @@ -0,0 +1,83 @@ +'use strict'; + +const common = require('../common.js'); +const assert = require('assert'); + +const bench = common.createBenchmark(main, { + n: [50], + size: [1e3], + datasetName: ['objects', 'sets', 'maps', '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', + { + bar: '123', + a: [1, 2, 3], + c: {}, + b: !depth ? createSets(2, depth + 1) : new Set(), + }, + ])); +} + +function createMaps(length, depth = 0) { + return Array.from({ length }, () => new Map([ + ['foo', 'yarp'], + ['nope', new Map([ + ['bar', '123'], + ['a', [1, 2, 3]], + ['c', {}], + ['b', !depth ? createMaps(2, depth + 1) : new Map()], + ])], + ])); +} + +function createArrayBuffers(length) { + return Array.from({ length }, (_, n) => { + return new ArrayBuffer(n); + }); +} + +function createDataViewArrayBuffers(length) { + return Array.from({ length }, (_, n) => { + return new DataView(new ArrayBuffer(n)); + }); +} + +const datasetMappings = { + objects: createObjects, + sets: createSets, + maps: createMaps, + 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..f27682f89523b6 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; } @@ -476,23 +474,37 @@ function partiallyCompareArrayBuffersOrViews(actual, expected) { 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(); - const actualArray = ArrayFrom(FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual)); const expectedIterator = FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], expected); - const usedIndices = new SafeSet(); + let actualSet; + + for (const expectedItem of expectedIterator) { + let foundMatch = 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; + // Check for primitives first to avoid useless loops + if (isPrimitive(expectedItem)) { + if (actual.has(expectedItem)) { + foundMatch = true; + } + } else { + actualSet ??= new SafeSet(FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual)); + + for (const actualItem of actualSet) { + if (isDeepStrictEqual(actualItem, expectedItem)) { + actualSet.delete(actualItem); + foundMatch = true; + break; + } } } - return false; + + if (!foundMatch) { + return false; + } } return true; @@ -518,13 +530,12 @@ 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 + 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 +551,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); @@ -723,6 +732,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 +754,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,