Skip to content

Commit 762399f

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

File tree

2 files changed

+127
-21
lines changed

2 files changed

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

lib/assert.js

Lines changed: 38 additions & 21 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
}
@@ -481,18 +479,38 @@ function partiallyCompareSets(actual, expected, comparedObjects) {
481479

482480
if (isDeepEqual === undefined) lazyLoadComparison();
483481

484-
const actualArray = ArrayFrom(FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual));
482+
// Create a map for faster lookups
483+
const actualMap = new SafeMap();
484+
const actualIterator = FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual);
485485
const expectedIterator = FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], expected);
486-
const usedIndices = new SafeSet();
487486

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;
487+
for (const actualItem of actualIterator) {
488+
actualMap.set(actualItem, true);
489+
}
490+
491+
for (const expectedItem of expectedIterator) {
492+
let foundMatch = false;
493+
494+
// Check for primitives first to avoid useless loops
495+
if (isPrimitive(expectedItem)) {
496+
if (actualMap.has(expectedItem)) {
497+
actualMap.delete(expectedItem);
498+
foundMatch = true;
499+
}
500+
} else {
501+
// Check for non-primitives
502+
for (const { 0: actualItem } of actualMap) {
503+
if (isDeepStrictEqual(actualItem, expectedItem)) {
504+
actualMap.delete(actualItem);
505+
foundMatch = true;
506+
break;
507+
}
493508
}
494509
}
495-
return false;
510+
511+
if (!foundMatch) {
512+
return false;
513+
}
496514
}
497515

498516
return true;
@@ -518,13 +536,11 @@ function partiallyCompareArrays(actual, expected, comparedObjects) {
518536

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

523-
for (const expectedItem of safeExpected) {
524-
// Check if the item is a zero or a -0, as these need to be handled separately
540+
for (const expectedItem of new SafeArrayIterator(expected)) {
525541
if (expectedItem === 0) {
526542
const zeroKey = getZeroKey(expectedItem);
527-
expectedCounts.set(zeroKey, (expectedCounts.get(zeroKey)?.count || 0) + 1);
543+
expectedCounts.set(zeroKey, (expectedCounts.get(zeroKey) ?? 0) + 1);
528544
} else {
529545
let found = false;
530546
for (const { 0: key, 1: count } of expectedCounts) {
@@ -540,10 +556,7 @@ function partiallyCompareArrays(actual, expected, comparedObjects) {
540556
}
541557
}
542558

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
559+
for (const actualItem of new SafeArrayIterator(actual)) {
547560
if (actualItem === 0) {
548561
const zeroKey = getZeroKey(actualItem);
549562

@@ -723,6 +736,10 @@ function compareExceptionKey(actual, expected, key, message, keys, fn) {
723736
}
724737
}
725738

739+
function isPrimitive(value) {
740+
return typeof value !== 'object' || value === null;
741+
}
742+
726743
function expectedException(actual, expected, message, fn) {
727744
let generatedMessage = false;
728745
let throwError = false;
@@ -741,7 +758,7 @@ function expectedException(actual, expected, message, fn) {
741758
}
742759
throwError = true;
743760
// Handle primitives properly.
744-
} else if (typeof actual !== 'object' || actual === null) {
761+
} else if (isPrimitive(actual)) {
745762
const err = new AssertionError({
746763
actual,
747764
expected,

0 commit comments

Comments
 (0)