Skip to content

Commit a07cf19

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

File tree

2 files changed

+135
-22
lines changed

2 files changed

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

lib/assert.js

Lines changed: 52 additions & 22 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
}
@@ -476,23 +474,44 @@ function partiallyCompareArrayBuffersOrViews(actual, expected) {
476474

477475
function partiallyCompareSets(actual, expected, comparedObjects) {
478476
if (SetPrototypeGetSize(expected) > SetPrototypeGetSize(actual)) {
479-
return false; // `expected` can't be a subset if it has more elements
477+
return false;
480478
}
481479

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

484-
const actualArray = ArrayFrom(FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual));
485482
const expectedIterator = FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], expected);
486-
const usedIndices = new SafeSet();
483+
let actualSet;
484+
485+
for (const expectedItem of expectedIterator) {
486+
let foundMatch = false;
487+
488+
// Check for direct inclusion first (fast path for both primitives and identical object references)
489+
if (actual.has(expectedItem)) {
490+
foundMatch = true;
491+
} else if (!isPrimitive(expectedItem)) {
492+
// Only create actualSet if we need it, and only add objects to it
493+
if (actualSet === undefined) {
494+
actualSet = new SafeSet();
495+
496+
for (const item of FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual)) {
497+
if (!isPrimitive(item)) {
498+
actualSet.add(item);
499+
}
500+
}
501+
}
487502

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;
503+
for (const actualItem of actualSet) {
504+
if (isDeepStrictEqual(actualItem, expectedItem)) {
505+
actualSet.delete(actualItem);
506+
foundMatch = true;
507+
break;
508+
}
493509
}
494510
}
495-
return false;
511+
512+
if (!foundMatch) {
513+
return false;
514+
}
496515
}
497516

498517
return true;
@@ -510,21 +529,26 @@ function getZeroKey(item) {
510529
}
511530

512531
function partiallyCompareArrays(actual, expected, comparedObjects) {
532+
if (actual === expected) return true;
533+
513534
if (expected.length > actual.length) {
514535
return false;
515536
}
516537

538+
if (expected.length === 0) {
539+
return true;
540+
}
541+
517542
if (isDeepEqual === undefined) lazyLoadComparison();
518543

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

523-
for (const expectedItem of safeExpected) {
524-
// Check if the item is a zero or a -0, as these need to be handled separately
547+
const expectedIterator = new SafeArrayIterator(expected);
548+
for (const expectedItem of expectedIterator) {
525549
if (expectedItem === 0) {
526550
const zeroKey = getZeroKey(expectedItem);
527-
expectedCounts.set(zeroKey, (expectedCounts.get(zeroKey)?.count || 0) + 1);
551+
expectedCounts.set(zeroKey, (expectedCounts.get(zeroKey) ?? 0) + 1);
528552
} else {
529553
let found = false;
530554
for (const { 0: key, 1: count } of expectedCounts) {
@@ -540,10 +564,8 @@ function partiallyCompareArrays(actual, expected, comparedObjects) {
540564
}
541565
}
542566

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
567+
const actualIterator = new SafeArrayIterator(actual);
568+
for (const actualItem of actualIterator) {
547569
if (actualItem === 0) {
548570
const zeroKey = getZeroKey(actualItem);
549571

@@ -567,6 +589,10 @@ function partiallyCompareArrays(actual, expected, comparedObjects) {
567589
}
568590
}
569591
}
592+
593+
if (expectedCounts.size === 0) {
594+
return true;
595+
}
570596
}
571597

572598
return expectedCounts.size === 0;
@@ -723,6 +749,10 @@ function compareExceptionKey(actual, expected, key, message, keys, fn) {
723749
}
724750
}
725751

752+
function isPrimitive(value) {
753+
return typeof value !== 'object' || value === null;
754+
}
755+
726756
function expectedException(actual, expected, message, fn) {
727757
let generatedMessage = false;
728758
let throwError = false;
@@ -741,7 +771,7 @@ function expectedException(actual, expected, message, fn) {
741771
}
742772
throwError = true;
743773
// Handle primitives properly.
744-
} else if (typeof actual !== 'object' || actual === null) {
774+
} else if (isPrimitive(actual)) {
745775
const err = new AssertionError({
746776
actual,
747777
expected,

0 commit comments

Comments
 (0)