Skip to content

Commit 7918fe6

Browse files
committed
Avoid expensive DeepChecker instance property initializations.
1 parent 0d2002a commit 7918fe6

File tree

2 files changed

+56
-12
lines changed

2 files changed

+56
-12
lines changed

packages/equality/src/checker.ts

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,19 +51,59 @@ const CHECKERS_BY_TAG = new Map<string, Checker<any>>()
5151
.set('[object AsyncGeneratorFunction]', checkFunctions)
5252
.set('[object Function]', checkFunctions);
5353

54+
type ComparisonTrie = Trie<{
55+
equal?: boolean;
56+
}>;
57+
58+
// Initializing checker.comparisons and checker.boundCheck as proper members of
59+
// the DeepChecker class makes creating DeepChecker objects considerably more
60+
// expensive in some environments, even if we initialize them to null and then
61+
// upgrade them lazily, when needed. Instead, we store these two items of state
62+
// in a separate Map, which gets cleaned up in the DeepChecker#release method.
63+
const privateStateMap = new Map<DeepChecker, {
64+
comparisons?: ComparisonTrie;
65+
boundCheck?: DeepEqualsHelper;
66+
}>();
67+
68+
function getPrivateState(checker: DeepChecker) {
69+
let state = privateStateMap.get(checker)!;
70+
if (!state) privateStateMap.set(checker, state = Object.create(null));
71+
return state;
72+
}
73+
74+
function getComparisons(checker: DeepChecker): ComparisonTrie {
75+
const state = getPrivateState(checker);
76+
return state.comparisons || (state.comparisons = new Trie(false));
77+
}
78+
5479
function getBoundCheck(checker: DeepChecker): DeepEqualsHelper {
55-
return checker["boundCheck"] || (checker["boundCheck"] = function (a, b) {
56-
return checker.check(a, b);
57-
});
80+
const state = getPrivateState(checker);
81+
return state.boundCheck || (
82+
state.boundCheck = (a, b) => checker.check(a, b)
83+
);
5884
}
5985

86+
const checkerPool: DeepChecker[] = [];
87+
const CHECKER_POOL_TARGET_SIZE = 5;
88+
6089
export class DeepChecker {
61-
// Initialized lazily because not always needed.
62-
private comparisons: null | Trie<{ equal?: boolean; }> = null;
90+
// Use DeepChecker.acquire() instead of new DeepChecker.
91+
protected constructor() {}
6392

64-
// Initialized lazily because needed only when custom deepEqualsMethod methods
65-
// are in use.
66-
private boundCheck: null | DeepEqualsHelper = null;
93+
static acquire() {
94+
return checkerPool.pop() || new DeepChecker();
95+
}
96+
97+
public release() {
98+
// If privateStateMap was a WeakMap, we wouldn't necessarily need to perform
99+
// this cleanup, but not all environments have a (performant) implementation
100+
// of WeakMap, and the cleanup is easy enough:
101+
privateStateMap.delete(this);
102+
103+
if (checkerPool.length < CHECKER_POOL_TARGET_SIZE) {
104+
checkerPool.push(this);
105+
}
106+
}
67107

68108
public check(a: any, b: any): boolean {
69109
// If the two values are strictly equal, our job is easy.
@@ -89,9 +129,7 @@ export class DeepChecker {
89129

90130
const found =
91131
bothNonNullObjects &&
92-
(this.comparisons || (
93-
this.comparisons = new Trie(false)
94-
)).lookup(a, b);
132+
getComparisons(this).lookup(a, b);
95133

96134
// Though cyclic references can make an object graph appear infinite from
97135
// the perspective of a depth-first traversal, the graph still contains a

packages/equality/src/equality.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ import { DeepChecker } from "./checker";
44
* Performs a deep equality check on two JavaScript values, tolerating cycles.
55
*/
66
export function equal(a: any, b: any): boolean {
7-
return a === b || new DeepChecker().check(a, b);
7+
if (a === b) return true;
8+
const checker = DeepChecker.acquire();
9+
try {
10+
return checker.check(a, b);
11+
} finally {
12+
checker.release();
13+
}
814
}
915

1016
// Allow default imports as well.

0 commit comments

Comments
 (0)