Skip to content

Commit 2e4fbdb

Browse files
committed
assert: improve partialDeepStrictEqual performance and add benchmark
1 parent 6b3937a commit 2e4fbdb

File tree

2 files changed

+173
-23
lines changed

2 files changed

+173
-23
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
const assert = require('assert');
5+
6+
const bench = common.createBenchmark(main, {
7+
n: [10, 50, 200],
8+
size: [1e3],
9+
datasetName: ['objects', 'simpleSets', 'complexSets', 'maps', 'arrayBuffers', 'dataViewArrayBuffers'],
10+
});
11+
12+
function createObjects(length, depth = 0) {
13+
return Array.from({ length }, () => ({
14+
foo: 'yarp',
15+
nope: {
16+
bar: '123',
17+
a: [1, 2, 3],
18+
c: {},
19+
b: !depth ? createObjects(2, depth + 1) : [],
20+
},
21+
}));
22+
}
23+
24+
function createSimpleSets(length, depth = 0) {
25+
return Array.from({ length }, () => new Set([
26+
'yarp',
27+
'123',
28+
1,
29+
2,
30+
3,
31+
null,
32+
]));
33+
}
34+
35+
function createComplexSets(length, depth = 0) {
36+
return Array.from({ length }, () => new Set([
37+
'yarp',
38+
{
39+
bar: '123',
40+
a: [1, 2, 3],
41+
c: {},
42+
b: !depth ? createComplexSets(2, depth + 1) : new Set(),
43+
},
44+
]));
45+
}
46+
47+
function createMaps(length, depth = 0) {
48+
return Array.from({ length }, () => new Map([
49+
['foo', 'yarp'],
50+
['nope', new Map([
51+
['bar', '123'],
52+
['a', [1, 2, 3]],
53+
['c', {}],
54+
['b', !depth ? createMaps(2, depth + 1) : new Map()],
55+
])],
56+
]));
57+
}
58+
59+
function createArrayBuffers(length) {
60+
return Array.from({ length }, (_, n) => {
61+
return new ArrayBuffer(n);
62+
});
63+
}
64+
65+
function createDataViewArrayBuffers(length) {
66+
return Array.from({ length }, (_, n) => {
67+
return new DataView(new ArrayBuffer(n));
68+
});
69+
}
70+
71+
const datasetMappings = {
72+
objects: createObjects,
73+
simpleSets: createSimpleSets,
74+
complexSets: createComplexSets,
75+
maps: createMaps,
76+
arrayBuffers: createArrayBuffers,
77+
dataViewArrayBuffers: createDataViewArrayBuffers,
78+
};
79+
80+
function getDatasets(datasetName, size) {
81+
return {
82+
actual: datasetMappings[datasetName](size),
83+
expected: datasetMappings[datasetName](size),
84+
};
85+
}
86+
87+
function main({ size, n, datasetName }) {
88+
const { actual, expected } = getDatasets(datasetName, size);
89+
90+
bench.start();
91+
for (let i = 0; i < n; ++i) {
92+
assert.partialDeepStrictEqual(actual, expected);
93+
}
94+
bench.end(n);
95+
}

lib/assert.js

Lines changed: 78 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
const {
2424
ArrayBufferIsView,
2525
ArrayBufferPrototypeGetByteLength,
26-
ArrayFrom,
2726
ArrayIsArray,
2827
ArrayPrototypeIndexOf,
2928
ArrayPrototypeJoin,
@@ -395,12 +394,11 @@ function partiallyCompareMaps(actual, expected, comparedObjects) {
395394
const expectedIterator = FunctionPrototypeCall(SafeMap.prototype[SymbolIterator], expected);
396395

397396
for (const { 0: key, 1: expectedValue } of expectedIterator) {
398-
if (!MapPrototypeHas(actual, key)) {
397+
const actualValue = MapPrototypeGet(actual, key);
398+
if (actualValue === undefined && !MapPrototypeHas(actual, key)) {
399399
return false;
400400
}
401401

402-
const actualValue = MapPrototypeGet(actual, key);
403-
404402
if (!compareBranch(actualValue, expectedValue, comparedObjects)) {
405403
return false;
406404
}
@@ -474,28 +472,74 @@ function partiallyCompareArrayBuffersOrViews(actual, expected) {
474472
return true;
475473
}
476474

475+
// Adapted version of the "setEquiv" function in lib/internal/util/comparisons.js
477476
function partiallyCompareSets(actual, expected, comparedObjects) {
478477
if (SetPrototypeGetSize(expected) > SetPrototypeGetSize(actual)) {
479-
return false; // `expected` can't be a subset if it has more elements
478+
return false;
480479
}
481480

482481
if (isDeepEqual === undefined) lazyLoadComparison();
482+
let set = null;
483483

484-
const actualArray = ArrayFrom(FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual));
484+
// First, check if elements from expected exist in actual
485485
const expectedIterator = FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], expected);
486-
const usedIndices = new SafeSet();
486+
for (const val of expectedIterator) {
487+
// Fast path: direct inclusion check for both primitives and reference equality
488+
if (actual.has(val)) {
489+
continue;
490+
}
491+
492+
// For primitives, if not found directly, return false immediately
493+
if (isPrimitive(val)) {
494+
return false;
495+
}
487496

488-
expectedIteration: for (const expectedItem of expectedIterator) {
489-
for (let actualIdx = 0; actualIdx < actualArray.length; actualIdx++) {
490-
if (!usedIndices.has(actualIdx) && isDeepStrictEqual(actualArray[actualIdx], expectedItem)) {
491-
usedIndices.add(actualIdx);
492-
continue expectedIteration;
497+
if (set === null) {
498+
// Special case to avoid set creation for single-element comparison
499+
if (SetPrototypeGetSize(expected) === 1) {
500+
// Try to find any deep-equal object in actual
501+
const actualIterator = FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual);
502+
for (const actualItem of actualIterator) {
503+
if (!isPrimitive(actualItem) && isDeepStrictEqual(actualItem, val)) {
504+
return true;
505+
}
506+
}
507+
return false;
493508
}
509+
set = new SafeSet();
494510
}
495-
return false;
511+
512+
// Add this object for later deep comparison
513+
set.add(val);
496514
}
497515

498-
return true;
516+
// If all items were found directly, we're done
517+
if (set === null) {
518+
return true;
519+
}
520+
521+
// For remaining objects that need deep comparison
522+
const actualIterator = FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual);
523+
for (const actualItem of actualIterator) {
524+
// Only consider non-primitive values for deep comparison
525+
if (!isPrimitive(actualItem)) {
526+
// Check if this actual item deep-equals any remaining expected item
527+
for (const expectedItem of set) {
528+
if (isDeepStrictEqual(actualItem, expectedItem)) {
529+
// Remove the matched item so we don't match it again
530+
set.delete(expectedItem);
531+
// If all items are matched, we can return early
532+
if (set.size === 0) {
533+
return true;
534+
}
535+
break;
536+
}
537+
}
538+
}
539+
}
540+
541+
// If all objects in expected found matches, set will be empty
542+
return set.size === 0;
499543
}
500544

501545
const minusZeroSymbol = Symbol('-0');
@@ -510,21 +554,26 @@ function getZeroKey(item) {
510554
}
511555

512556
function partiallyCompareArrays(actual, expected, comparedObjects) {
557+
if (actual === expected) return true;
558+
513559
if (expected.length > actual.length) {
514560
return false;
515561
}
516562

563+
if (expected.length === 0) {
564+
return true;
565+
}
566+
517567
if (isDeepEqual === undefined) lazyLoadComparison();
518568

519569
// Create a map to count occurrences of each element in the expected array
520570
const expectedCounts = new SafeMap();
521-
const safeExpected = new SafeArrayIterator(expected);
522571

523-
for (const expectedItem of safeExpected) {
524-
// Check if the item is a zero or a -0, as these need to be handled separately
572+
const expectedIterator = new SafeArrayIterator(expected);
573+
for (const expectedItem of expectedIterator) {
525574
if (expectedItem === 0) {
526575
const zeroKey = getZeroKey(expectedItem);
527-
expectedCounts.set(zeroKey, (expectedCounts.get(zeroKey)?.count || 0) + 1);
576+
expectedCounts.set(zeroKey, (expectedCounts.get(zeroKey) ?? 0) + 1);
528577
} else {
529578
let found = false;
530579
for (const { 0: key, 1: count } of expectedCounts) {
@@ -540,10 +589,8 @@ function partiallyCompareArrays(actual, expected, comparedObjects) {
540589
}
541590
}
542591

543-
const safeActual = new SafeArrayIterator(actual);
544-
545-
for (const actualItem of safeActual) {
546-
// Check if the item is a zero or a -0, as these need to be handled separately
592+
const actualIterator = new SafeArrayIterator(actual);
593+
for (const actualItem of actualIterator) {
547594
if (actualItem === 0) {
548595
const zeroKey = getZeroKey(actualItem);
549596

@@ -567,6 +614,10 @@ function partiallyCompareArrays(actual, expected, comparedObjects) {
567614
}
568615
}
569616
}
617+
618+
if (expectedCounts.size === 0) {
619+
return true;
620+
}
570621
}
571622

572623
return expectedCounts.size === 0;
@@ -723,6 +774,10 @@ function compareExceptionKey(actual, expected, key, message, keys, fn) {
723774
}
724775
}
725776

777+
function isPrimitive(value) {
778+
return typeof value !== 'object' || value === null;
779+
}
780+
726781
function expectedException(actual, expected, message, fn) {
727782
let generatedMessage = false;
728783
let throwError = false;
@@ -741,7 +796,7 @@ function expectedException(actual, expected, message, fn) {
741796
}
742797
throwError = true;
743798
// Handle primitives properly.
744-
} else if (typeof actual !== 'object' || actual === null) {
799+
} else if (isPrimitive(actual)) {
745800
const err = new AssertionError({
746801
actual,
747802
expected,

0 commit comments

Comments
 (0)