From ad7dbff10c9abab5e85b6055dd68c25a501c43fe Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Thu, 24 Aug 2023 17:58:54 +0300 Subject: [PATCH] introduce OrderedSet to optimize comparisons and insertion into the set-indexed map --- src/execution/collectFields.ts | 18 +++-- src/jsutils/OrderedSet.ts | 93 ++++++++++++++++++++++++ src/jsutils/__tests__/OrderedSet-test.ts | 34 +++++++++ src/jsutils/getBySet.ts | 13 ---- src/jsutils/isSameSet.ts | 14 ---- 5 files changed, 137 insertions(+), 35 deletions(-) create mode 100644 src/jsutils/OrderedSet.ts create mode 100644 src/jsutils/__tests__/OrderedSet-test.ts delete mode 100644 src/jsutils/getBySet.ts delete mode 100644 src/jsutils/isSameSet.ts diff --git a/src/execution/collectFields.ts b/src/execution/collectFields.ts index 1d0341b4cc1..43b36343ebc 100644 --- a/src/execution/collectFields.ts +++ b/src/execution/collectFields.ts @@ -1,8 +1,8 @@ import { AccumulatorMap } from '../jsutils/AccumulatorMap.js'; -import { getBySet } from '../jsutils/getBySet.js'; import { invariant } from '../jsutils/invariant.js'; -import { isSameSet } from '../jsutils/isSameSet.js'; import type { ObjMap } from '../jsutils/ObjMap.js'; +import type { ReadonlyOrderedSet } from '../jsutils/OrderedSet.js'; +import { OrderedSet } from '../jsutils/OrderedSet.js'; import type { FieldNode, @@ -33,11 +33,13 @@ export interface DeferUsage { ancestors: ReadonlyArray; } -export const NON_DEFERRED_TARGET_SET: TargetSet = new Set([undefined]); +export const NON_DEFERRED_TARGET_SET = new OrderedSet([ + undefined, +]).freeze(); export type Target = DeferUsage | undefined; -export type TargetSet = ReadonlySet; -export type DeferUsageSet = ReadonlySet; +export type TargetSet = ReadonlyOrderedSet; +export type DeferUsageSet = ReadonlyOrderedSet; export interface FieldDetails { node: FieldNode; @@ -430,13 +432,13 @@ function getTargetSetDetails( } } - const maskingTargets: TargetSet = new Set(maskingTargetList); - if (isSameSet(maskingTargets, parentTargets)) { + const maskingTargets = new OrderedSet(maskingTargetList).freeze(); + if (maskingTargets === parentTargets) { parentTargetKeys.add(responseKey); continue; } - let targetSetDetails = getBySet(targetSetDetailsMap, maskingTargets); + let targetSetDetails = targetSetDetailsMap.get(maskingTargets); if (targetSetDetails === undefined) { targetSetDetails = { keys: new Set(), diff --git a/src/jsutils/OrderedSet.ts b/src/jsutils/OrderedSet.ts new file mode 100644 index 00000000000..3cb97977bbe --- /dev/null +++ b/src/jsutils/OrderedSet.ts @@ -0,0 +1,93 @@ +const setContainingUndefined = new Set([undefined]); +const setsContainingOneItem = new WeakMap>(); +const setsAppendedByUndefined = new WeakMap< + ReadonlySet, + Set +>(); +const setsAppendedByDefined = new WeakMap< + ReadonlySet, + WeakMap> +>(); + +function createOrderedSet( + item: T, +): ReadonlySet { + if (item === undefined) { + return setContainingUndefined; + } + + let set = setsContainingOneItem.get(item); + if (set === undefined) { + set = new Set([item]); + set.add(item); + setsContainingOneItem.set(item, set); + } + return set as ReadonlyOrderedSet; +} + +function appendToOrderedSet( + set: ReadonlySet, + item: T | undefined, +): ReadonlySet { + if (set.has(item)) { + return set; + } + + if (item === undefined) { + let appendedSet = setsAppendedByUndefined.get(set); + if (appendedSet === undefined) { + appendedSet = new Set(set); + appendedSet.add(undefined); + setsAppendedByUndefined.set(set, appendedSet); + } + return appendedSet as ReadonlySet; + } + + let appendedSets = setsAppendedByDefined.get(set); + if (appendedSets === undefined) { + appendedSets = new WeakMap(); + setsAppendedByDefined.set(set, appendedSets); + const appendedSet = new Set(set); + appendedSet.add(item); + appendedSets.set(item, appendedSet); + return appendedSet as ReadonlySet; + } + + let appendedSet: Set | undefined = appendedSets.get(item); + if (appendedSet === undefined) { + appendedSet = new Set(set); + appendedSet.add(item); + appendedSets.set(item, appendedSet); + } + + return appendedSet as ReadonlySet; +} + +export type ReadonlyOrderedSet = ReadonlySet; + +const emptySet = new Set(); + +/** + * A set that when frozen can be directly compared for equality. + * + * Sets are limited to JSON serializable values. + * + * @internal + */ +export class OrderedSet { + _set: ReadonlySet = emptySet as ReadonlySet; + constructor(items: Iterable) { + for (const item of items) { + if (this._set === emptySet) { + this._set = createOrderedSet(item); + continue; + } + + this._set = appendToOrderedSet(this._set, item); + } + } + + freeze(): ReadonlyOrderedSet { + return this._set as ReadonlyOrderedSet; + } +} diff --git a/src/jsutils/__tests__/OrderedSet-test.ts b/src/jsutils/__tests__/OrderedSet-test.ts new file mode 100644 index 00000000000..445053a32a3 --- /dev/null +++ b/src/jsutils/__tests__/OrderedSet-test.ts @@ -0,0 +1,34 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import { OrderedSet } from '../OrderedSet.js'; + +describe('OrderedSet', () => { + it('empty sets are equal', () => { + const orderedSetA = new OrderedSet([]).freeze(); + const orderedSetB = new OrderedSet([]).freeze(); + + expect(orderedSetA).to.equal(orderedSetB); + }); + + it('sets with members in different orders or numbers are equal', () => { + const a = { a: 'a' }; + const b = { b: 'b' }; + const c = { c: 'c' }; + const orderedSetA = new OrderedSet([a, b, c, a, undefined]).freeze(); + const orderedSetB = new OrderedSet([undefined, b, a, b, c]).freeze(); + + expect(orderedSetA).to.not.equal(orderedSetB); + }); + + it('sets with members in different orders or numbers are equal', () => { + const a = { a: 'a' }; + const b = { b: 'b' }; + const c = { c: 'c' }; + const d = { c: 'd' }; + const orderedSetA = new OrderedSet([a, b, c, a, undefined]).freeze(); + const orderedSetB = new OrderedSet([undefined, b, a, b, d]).freeze(); + + expect(orderedSetA).to.not.equal(orderedSetB); + }); +}); diff --git a/src/jsutils/getBySet.ts b/src/jsutils/getBySet.ts deleted file mode 100644 index 4ddabd30021..00000000000 --- a/src/jsutils/getBySet.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { isSameSet } from './isSameSet.js'; - -export function getBySet( - map: ReadonlyMap, U>, - setToMatch: ReadonlySet, -): U | undefined { - for (const set of map.keys()) { - if (isSameSet(set, setToMatch)) { - return map.get(set); - } - } - return undefined; -} diff --git a/src/jsutils/isSameSet.ts b/src/jsutils/isSameSet.ts deleted file mode 100644 index 4e3575167d2..00000000000 --- a/src/jsutils/isSameSet.ts +++ /dev/null @@ -1,14 +0,0 @@ -export function isSameSet( - setA: ReadonlySet, - setB: ReadonlySet, -): boolean { - if (setA.size !== setB.size) { - return false; - } - for (const item of setA) { - if (!setB.has(item)) { - return false; - } - } - return true; -}