From eeb8c71af3378f3a1bb3641464a84ff6506cecb3 Mon Sep 17 00:00:00 2001 From: mspiess <30656914+mspiess@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:36:50 +0200 Subject: [PATCH 1/4] test: add benchmarks --- .github/workflows/node.js.yml | 2 + package.json | 1 + src/main.benchmark.ts | 102 ++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 src/main.benchmark.ts diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 9d1bfa2..ea28844 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -37,3 +37,5 @@ jobs: run: npm run test:mutation env: STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} + - if: ${{ matrix.node-version == '26.x' }} + run: npm run benchmark diff --git a/package.json b/package.json index 68aceb3..4eb7d6d 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "lint": "eslint src/", "test": "vitest --typecheck --run", "test:mutation": "stryker run", + "benchmark": "vitest bench", "changeset": "changeset", "release": "npm run build && changeset tag && npm publish --provenance --access public" }, diff --git a/src/main.benchmark.ts b/src/main.benchmark.ts new file mode 100644 index 0000000..cbf0334 --- /dev/null +++ b/src/main.benchmark.ts @@ -0,0 +1,102 @@ +import { describe, expect, test } from 'vitest'; +import type { IntegerLessThan32 } from './IntegerLessThan32.ts'; +import { EnumSet } from './main.ts'; + +const allValues = [...Array(32).keys()] as IntegerLessThan32[]; + +describe('has', () => { + test('empty set', async ({ bench }) => { + const nativeSet = new Set(); + const enumSet = new EnumSet(); + + await bench.compare( + bench('native set', () => { + allValues.forEach((value) => { + nativeSet.has(value); + }); + }), bench('enum set', () => { + allValues.forEach((value) => { + enumSet.has(value); + }); + }), + ); + }); + + test('full set', async ({ bench }) => { + const nativeSet = new Set(allValues); + const enumSet = new EnumSet(allValues); + + await bench.compare( + bench('native set', () => { + allValues.forEach((value) => { + nativeSet.has(value); + }); + }), bench('enum set', () => { + allValues.forEach((value) => { + enumSet.has(value); + }); + }), + ); + }); +}); + +test('add', async ({ bench }) => { + const nativeSet = new Set(); + const enumSet = new EnumSet(); + + const result = await bench.compare( + bench('native set', () => { + allValues.forEach((value) => { + nativeSet.add(value); + }); + }), bench('enum set', () => { + allValues.forEach((value) => { + enumSet.add(value); + }); + }), + ); + + expect([...nativeSet.values()]).toStrictEqual(allValues); + expect([...enumSet.values()]).toStrictEqual(allValues); + expect(result.get('enum set')).toBeFasterThan(result.get('native set')); +}); + +test('delete', async ({ bench }) => { + const nativeSet = new Set(allValues); + const enumSet = new EnumSet(allValues); + + const result = await bench.compare( + bench('native set', () => { + allValues.forEach((value) => { + nativeSet.delete(value); + }); + }), bench('enum set', () => { + allValues.forEach((value) => { + enumSet.delete(value); + }); + }), + ); + + expect(nativeSet.size).toBe(0); + expect(enumSet.size).toBe(0); + expect(result.get('enum set')).toBeFasterThan(result.get('native set')); +}); + +test.for([ + { method: 'keys' as const }, + { method: 'values' as const }, + // workaround for https://github.com/microsoft/TypeScript/issues/54100 + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + { method: Symbol.iterator } as { method: typeof Symbol.iterator }, +])('$method', async ({ method }, { bench }) => { + const nativeSet = new Set(allValues); + const enumSet = new EnumSet(allValues); + + await bench.compare( + bench('native set', () => { + nativeSet[method](); + }), bench('enum set', () => { + enumSet[method](); + }), + ); +}); From b2b61c30e9b62b0c157f9e28f5ef9a8f8e8ff96b Mon Sep 17 00:00:00 2001 From: mspiess <30656914+mspiess@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:36:50 +0200 Subject: [PATCH 2/4] test: add benchmarks for composition methods --- src/main.benchmark.ts | 103 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/src/main.benchmark.ts b/src/main.benchmark.ts index cbf0334..4a9f4e1 100644 --- a/src/main.benchmark.ts +++ b/src/main.benchmark.ts @@ -3,6 +3,8 @@ import type { IntegerLessThan32 } from './IntegerLessThan32.ts'; import { EnumSet } from './main.ts'; const allValues = [...Array(32).keys()] as IntegerLessThan32[]; +const lowerHalfValues = [...Array(16).keys()] as IntegerLessThan32[]; +const upperHalfValues = [...Array(16).keys()].map(value => value + 16) as IntegerLessThan32[]; describe('has', () => { test('empty set', async ({ bench }) => { @@ -100,3 +102,104 @@ test.for([ }), ); }); + +test.for<{ + operation: 'union' | 'intersection' | 'difference' | 'symmetricDifference'; + leftValues: IntegerLessThan32[]; + rightValues: IntegerLessThan32[]; + expectedValues: IntegerLessThan32[]; +}>([ + { + operation: 'union', + leftValues: lowerHalfValues, + rightValues: upperHalfValues, + expectedValues: allValues, + }, + { + operation: 'intersection', + leftValues: lowerHalfValues, + rightValues: allValues, + expectedValues: lowerHalfValues, + }, + { + operation: 'difference', + leftValues: allValues, + rightValues: upperHalfValues, + expectedValues: lowerHalfValues, + }, + { + operation: 'symmetricDifference', + leftValues: [0, 1, 2, 3], + rightValues: [2, 3, 4, 5], + expectedValues: [0, 1, 4, 5], + }, +])('$operation', async ({ operation, leftValues, rightValues, expectedValues }, { bench }) => { + const nativeSets = [new Set(leftValues), new Set(rightValues)]; + const enumSets = [new EnumSet(leftValues), new EnumSet(rightValues)]; + type CompositionMethod = typeof nativeSets[number][typeof operation]; + + let nativeResult: Set | undefined; + let enumSetResult: Set | undefined; + + const result = await bench.compare( + bench('native set', () => { + const [left, right] = nativeSets; + + nativeResult = left[operation](right); + }), + bench('enum set', () => { + const [left, right] = enumSets; + + enumSetResult = (left[operation] as CompositionMethod)(right) as EnumSet; + }), + ); + + expect([...nativeResult!.values()]).toEqual(expectedValues); + expect([...enumSetResult!.values()]).toEqual(expectedValues); + expect(result.get('enum set')).toBeFasterThan(result.get('native set')); +}); + +test.for<{ + operation: 'isDisjointFrom' | 'isSubsetOf' | 'isSupersetOf'; + leftValues: IntegerLessThan32[]; + rightValues: IntegerLessThan32[]; +}>([ + { + operation: 'isSubsetOf', + leftValues: lowerHalfValues, + rightValues: allValues, + }, + { + operation: 'isSupersetOf', + leftValues: allValues, + rightValues: lowerHalfValues, + }, + { + operation: 'isDisjointFrom', + leftValues: lowerHalfValues, + rightValues: upperHalfValues, + }, +])('$operation', async ({ operation, leftValues, rightValues }, { bench }) => { + const nativeSets = [new Set(leftValues), new Set(rightValues)]; + const enumSets = [new EnumSet(leftValues), new EnumSet(rightValues)]; + + let nativeResult = false; + let enumSetResult = false; + + const result = await bench.compare( + bench('native set', () => { + const [left, right] = nativeSets; + + nativeResult = left[operation](right); + }), + bench('enum set', () => { + const [left, right] = enumSets; + + enumSetResult = left[operation](right); + }), + ); + + expect(nativeResult).toBe(true); + expect(enumSetResult).toBe(true); + expect(result.get('enum set')).toBeFasterThan(result.get('native set')); +}); From c6e3ee014f50015e143aa9fefdc0994b1a19a072 Mon Sep 17 00:00:00 2001 From: mspiess <30656914+mspiess@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:36:50 +0200 Subject: [PATCH 3/4] docs: add benchmark section --- README.md | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 62d596b..621190a 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ All state is packed into a single 32-bit integer bitfield. ## Installation -``` +```shell npm install @marco.spiess/enumset ``` @@ -44,3 +44,80 @@ expect(enumSet.has(Directions.Left)).toBe(false); - Iteration order breaks the [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) contract. The methods `forEach`, `keys`, `values` and `[Symbol.iterator]` iterate in the ascending order of the enum values and *not* in insertion order. + +## Benchmark + +Compared to the native `Set` `EnumSets` perform better for `add` and `delete`. +For `has` and iteration `EnumSets` are slightly slower. +For all set composition methods `EnumSets` vastly outperform their native counterparts. + +
+Result of a benchmark run in CI + +``` + ✓ bench src/main.benchmark.ts (14 tests) 209391ms + ✓ has (2) + ✓ empty set 19811ms + name hz min max mean p75 p99 p995 p999 rme samples + native set 21,206,116.26 0.0000 0.1372 0.0000 0.0001 0.0001 0.0001 0.0001 ±0.09% 20636388 fastest + enum set 13,198,874.44 0.0001 0.0909 0.0001 0.0001 0.0001 0.0001 0.0001 ±0.08% 12924792 + ✓ full set 20078ms + name hz min max mean p75 p99 p995 p999 rme samples + native set 21,291,907.56 0.0000 0.0978 0.0000 0.0001 0.0001 0.0001 0.0001 ±0.10% 20736684 fastest + enum set 13,237,134.16 0.0001 0.0427 0.0001 0.0001 0.0001 0.0001 0.0001 ±0.07% 13005323 + ✓ add 11337ms + name hz min max mean p75 p99 p995 p999 rme samples + enum set 14,437,209.78 0.0001 0.0320 0.0001 0.0001 0.0001 0.0001 0.0001 ±0.07% 14219315 fastest + native set 4,048,108.96 0.0002 0.3157 0.0003 0.0003 0.0003 0.0003 0.0007 ±0.25% 3965984 + ✓ delete 11266ms + name hz min max mean p75 p99 p995 p999 rme samples + enum set 14,224,467.32 0.0001 0.0729 0.0001 0.0001 0.0001 0.0001 0.0001 ±0.08% 14015339 fastest + native set 4,287,069.25 0.0002 0.3568 0.0002 0.0002 0.0003 0.0004 0.0007 ±0.30% 4186946 + ✓ keys 22782ms + name hz min max mean p75 p99 p995 p999 rme samples + native set 22,845,624.54 0.0000 0.0415 0.0000 0.0001 0.0001 0.0001 0.0001 ±0.07% 22276472 fastest + enum set 22,849,172.68 0.0000 0.0951 0.0000 0.0001 0.0001 0.0001 0.0001 ±0.08% 22254698 + ✓ values 17108ms + name hz min max mean p75 p99 p995 p999 rme samples + native set 19,279,170.57 0.0000 0.0413 0.0001 0.0001 0.0001 0.0001 0.0001 ±0.08% 18898286 fastest + enum set 12,656,450.07 0.0001 18.8421 0.0001 0.0001 0.0001 0.0001 0.0005 ±4.09% 11694214 + ✓ Symbol(Symbol.iterator) 19436ms + name hz min max mean p75 p99 p995 p999 rme samples + native set 19,442,343.10 0.0000 0.0798 0.0001 0.0001 0.0001 0.0001 0.0001 ±0.08% 19006739 fastest + enum set 17,544,892.40 0.0000 66.8953 0.0001 0.0001 0.0001 0.0001 0.0004 ±13.17% 15254944 + ✓ union 12892ms + name hz min max mean p75 p99 p995 p999 rme samples + enum set 20,008,001.47 0.0000 0.1361 0.0001 0.0001 0.0001 0.0001 0.0001 ±0.08% 19682920 fastest + native set 2,583,085.76 0.0003 0.3434 0.0004 0.0004 0.0006 0.0007 0.0011 ±0.48% 2473422 + ✓ intersection 10870ms + name hz min max mean p75 p99 p995 p999 rme samples + enum set 14,711,000.12 0.0001 0.0299 0.0001 0.0001 0.0001 0.0001 0.0001 ±0.08% 14419920 fastest + native set 2,186,253.34 0.0004 0.4180 0.0005 0.0005 0.0006 0.0008 0.0011 ±0.37% 2127560 + ✓ difference 11318ms + name hz min max mean p75 p99 p995 p999 rme samples + enum set 13,911,659.27 0.0001 0.0480 0.0001 0.0001 0.0001 0.0001 0.0001 ±0.08% 13645951 fastest + native set 3,541,035.40 0.0002 0.0290 0.0003 0.0003 0.0004 0.0005 0.0009 ±0.08% 3463004 + ✓ symmetricDifference 11600ms + name hz min max mean p75 p99 p995 p999 rme samples + enum set 13,746,639.49 0.0001 0.0841 0.0001 0.0001 0.0001 0.0001 0.0001 ±0.11% 13438668 fastest + native set 5,153,365.76 0.0002 0.3619 0.0002 0.0002 0.0002 0.0003 0.0006 ±0.40% 4995671 + ✓ isSubsetOf 15365ms + name hz min max mean p75 p99 p995 p999 rme samples + enum set 21,584,121.56 0.0000 0.1773 0.0000 0.0001 0.0001 0.0001 0.0001 ±0.12% 20949499 fastest + native set 5,478,922.03 0.0002 0.0427 0.0002 0.0002 0.0002 0.0002 0.0004 ±0.07% 5405264 + ✓ isSupersetOf 12293ms + name hz min max mean p75 p99 p995 p999 rme samples + enum set 15,724,180.49 0.0000 0.0490 0.0001 0.0001 0.0001 0.0001 0.0001 ±0.07% 15426052 fastest + native set 4,863,504.30 0.0002 0.0346 0.0002 0.0002 0.0004 0.0004 0.0005 ±0.08% 4700775 + ✓ isDisjointFrom 13232ms + name hz min max mean p75 p99 p995 p999 rme samples + enum set 16,888,742.09 0.0000 0.0478 0.0001 0.0001 0.0001 0.0001 0.0001 ±0.08% 16538521 fastest + native set 4,836,225.74 0.0002 0.0411 0.0002 0.0002 0.0002 0.0003 0.0004 ±0.07% 4765987 +``` + +
+ +To run the benchmark: +```shell +npm run benchmark +``` \ No newline at end of file From 19a541ce3f2065c63cb8fae6b22cbaabde39fedf Mon Sep 17 00:00:00 2001 From: mspiess <30656914+mspiess@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:36:50 +0200 Subject: [PATCH 4/4] chore: add changeset --- .changeset/famous-islands-invite.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/famous-islands-invite.md diff --git a/.changeset/famous-islands-invite.md b/.changeset/famous-islands-invite.md new file mode 100644 index 0000000..0033bad --- /dev/null +++ b/.changeset/famous-islands-invite.md @@ -0,0 +1,5 @@ +--- +"@marco.spiess/enumset": patch +--- + +Add benchmark