Skip to content

Commit

Permalink
feat(perf): up performance from quadratic to linear time (#77)
Browse files Browse the repository at this point in the history
  • Loading branch information
dankreiger committed Nov 27, 2022
1 parent e06e0b4 commit 6277380
Show file tree
Hide file tree
Showing 29 changed files with 211 additions and 53 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/gh-packages-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
run: npm run lint

- name: Test
run: npm run test --ci --coverage --maxWorkers=2
run: npm run test:ci

- name: Build
run: npm run build
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
run: npm run lint

- name: Test
run: npm run test --ci --coverage --maxWorkers=2
run: npm run test:ci

- name: Build
run: npm run build
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
run: npm run lint

- name: Test
run: npm run test --ci --coverage --maxWorkers=2
run: npm run test:ci

- name: Build
run: npm run build
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
- name: Lint
run: npm run lint
- name: Test
run: npm run test --ci --coverage --maxWorkers=2
run: npm run test:ci
- name: Build
run: npm run build
- name: size-limit-action
Expand Down
75 changes: 46 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,34 +17,51 @@ const listGroup = [
const byAge = createGroup('age');

byAge(listGroup);
/**
* {
* entities: {
* '20': [
* {
* age: 20,
* name: 'Dan',
* },
* ],
* '22': [
* {
* age: 22,
* name: 'Woofer',
* },
* ],
* '5': [
* {
* age: 5,
* name: 'Dan',
* },
* {
* age: 5,
* name: 'Puppy',
* },
* ],
* },
* ids: [5, 22, 20],
* }
*/

/** yields **/

{
entities: {
'20': [
{
age: 20,
name: 'Dan',
},
],
'22': [
{
age: 22,
name: 'Woofer',
},
],
'5': [
{
age: 5,
name: 'Dan',
},
{
age: 5,
name: 'Puppy',
},
],
},
ids: [5, 22, 20],
}
const byName = createGroup('name');

byName(listGroup);

/**
* {
* entities: {
* Dan: [
* { name: 'Dan', age: 5 },
* { name: 'Dan', age: 20 }
* ],
* Puppy: [ { name: 'Puppy', age: 5 } ],
* Woofer: [ { name: 'Woofer', age: 22 } ]
* },
* ids: [ 'Dan', 'Puppy', 'Woofer' ]
* }
*/
```
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@
"release": "scripty",
"size": "scripty",
"size:analyze": "scripty",
"test": "scripty"
"test": "scripty",
"test:ci": "scripty",
"test:watch": "scripty"
},
"config": {
"commitizen": {
Expand Down Expand Up @@ -99,4 +101,4 @@
"limit": "1 KB"
}
]
}
}
3 changes: 1 addition & 2 deletions scripts/build/index.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@

#!/usr/bin/env sh
source ./scripts/shared.sh

npm run clean
echo "┏━━━ 📦 $PACKAGE_NAME: build ━━━━━━━━━━━━━━━━━━━"
echo "┏━━━ 📦 build ━━━━━━━━━━━━━━━━━━━"
tsc && rollup -c
3 changes: 1 addition & 2 deletions scripts/clean/index.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/usr/bin/env sh
source ./scripts/shared.sh

echo "┏━━━ 🧹 $PACKAGE_NAME: clean ━━━━━━━━━━━━━━━━━━━"
echo "┏━━━ 🧹 clean ━━━━━━━━━━━━━━━━━━━"
rimraf ./dist
3 changes: 1 addition & 2 deletions scripts/lint/index.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/usr/bin/env sh
source ./scripts/shared.sh

echo "┏━━━ 🕵️‍♀️ $PACKAGE_NAME lint ━━━━━━━"
echo "┏━━━ 🕵️‍♀️ lint ━━━━━━━"
eslint src --quiet --ext .ts
2 changes: 0 additions & 2 deletions scripts/shared.sh

This file was deleted.

3 changes: 1 addition & 2 deletions scripts/size/analyze.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/usr/bin/env sh
source ./scripts/shared.sh

echo "┏━━━ 🛍️ $PACKAGE_NAME size:analyze ━━━━━━━"
echo "┏━━━ 🛍️ size:analyze ━━━━━━━"
size-limit --why
3 changes: 1 addition & 2 deletions scripts/size/index.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/usr/bin/env sh
source ./scripts/shared.sh

echo "┏━━━ 🛍️ $PACKAGE_NAME: size ━━━━━━━"
echo "┏━━━ 🛍️ size ━━━━━━━"
size-limit
3 changes: 3 additions & 0 deletions scripts/test/ci.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env sh

node --expose-gc ./node_modules/.bin/jest --runInBand --coverage --verbose --forceExit --detectOpenHandles --ci
2 changes: 1 addition & 1 deletion scripts/test/index.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/usr/bin/env sh

npx jest
npx jest --runInBand --coverage --verbose --forceExit --detectOpenHandles
3 changes: 3 additions & 0 deletions scripts/test/watch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env sh

npx jest --runInBand --watchAll --forceExit --detectOpenHandles
12 changes: 12 additions & 0 deletions src/test-helpers/__fixtures__/TEN_ITEMS.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const TEN_ITEMS = [
{ name: 'Dan', age: 5 },
{ name: 'Puppy', age: 5 },
{ name: 'Woofer', age: 22 },
{ name: 'Dan', age: 20 },
{ name: 'Cat', age: 11 },
{ name: 'Dog', age: 2 },
{ name: 'Pig', age: 20 },
{ name: 'Cow', age: 6 },
{ name: 'Horse', age: 9 },
{ name: 'Pig', age: 2 },
];
1 change: 1 addition & 0 deletions src/test-helpers/__fixtures__/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './TEN_ITEMS';
7 changes: 7 additions & 0 deletions src/test-helpers/getNItems/getNItems.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import { TEN_ITEMS } from '../__fixtures__';

/* eslint-disable @typescript-eslint/no-unsafe-return */
export const getNItems = (count: number) =>
Array.from({ length: count / TEN_ITEMS.length }).flatMap(() => TEN_ITEMS);
1 change: 1 addition & 0 deletions src/test-helpers/getNItems/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './getNItems';
3 changes: 3 additions & 0 deletions src/test-helpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './getNItems';
export * from './logPerformance';
export * from './__fixtures__';
1 change: 1 addition & 0 deletions src/test-helpers/logPerformance/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './logPerformance';
16 changes: 16 additions & 0 deletions src/test-helpers/logPerformance/logPerformance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const logPerformance = (opts: {
name: string;
fn: () => void;
assertion: (actual: number) => void;
}) => {
const { name, fn, assertion } = opts;

const start = performance.now();
fn();
const end = performance.now();

const duration = end - start;
assertion(duration);

console.table({ [name]: `${duration}ms` });
};
2 changes: 1 addition & 1 deletion src/typings/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export type ObjectKey = string | number | symbol;

export type EntityDict<Entity, Key extends ObjectKey> = {
entities: Record<Key, Entity | undefined>;
entities: Record<Key, Entity[] | undefined>;
ids: Key[];
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { EntityDict } from '../../typings';

export const groupBy = <K extends keyof T, T extends Record<K, T[K]>>(
/**
* O(n^2) implementation of groupBy
*/
export const groupBySlow = <K extends keyof T, T extends Record<K, T[K]>>(
key: K,
items: T[]
) =>
Expand Down
23 changes: 23 additions & 0 deletions src/utils/__internal__/groupBy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { EntityDict } from '../../typings';

/**
* O(n) implementation of groupBy
*/
export const groupBy = <K extends keyof T, T extends Record<K, T[K]>>(
key: K,
items: T[]
) => {
const result = { entities: {}, ids: [] } as EntityDict<T, T[K]>;

for (let i = 0; i < items.length; i++) {
if (result.entities[items[i][key]]) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
result.entities[items[i][key]]!.push(items[i]);
} else {
result.entities[items[i][key]] = [items[i]];
result.ids.push(items[i][key]);
}
}

return result;
};
File renamed without changes.
72 changes: 72 additions & 0 deletions src/utils/createGroup/createGroup.perf.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { getNItems, logPerformance } from '../../test-helpers';
import { createGroup } from './createGroup';

const ONE_THOUSAND_ITEMS = getNItems(1000);
const TEN_THOUSAND_ITEMS = getNItems(10_000);
const ONE_MILLION_ITEMS = getNItems(1_000_000);
const TEN_MILLION_ITEMS = getNItems(10_000_000);

const TEN_MILLISECONDS = 10;
const ONE_HUNDRED_MILLISECONDS = 100;
const FIVE_HUNDRED_MILLISECONDS = 500;
const ONE_SECOND = 1000;

const byAge = createGroup('age');

describe('createGroup - performance', () => {
it('[One thousand items]: executes in less than 10ms', (done) => {
expect(ONE_THOUSAND_ITEMS.length).toBe(1000);

logPerformance({
name: 'One thousand items',
fn: () => byAge(ONE_THOUSAND_ITEMS),
assertion: (milliseconds) => {
expect(milliseconds).toBeLessThan(TEN_MILLISECONDS);
global.gc?.();
done();
},
});
});

test('[Ten thousand items]: executes in less than 100ms', (done) => {
expect(TEN_THOUSAND_ITEMS.length).toBe(10_000);

logPerformance({
name: 'Ten thousand items',
fn: () => byAge(TEN_THOUSAND_ITEMS),
assertion: (milliseconds) => {
expect(milliseconds).toBeLessThan(ONE_HUNDRED_MILLISECONDS);
global.gc?.();
done();
},
});
});
it('[One million items]: executes in less than 500ms', (done) => {
expect(ONE_MILLION_ITEMS.length).toBe(1_000_000);

logPerformance({
name: 'One Million Items',
fn: () => byAge(ONE_MILLION_ITEMS),
assertion: (actual) => {
expect(actual).toBeLessThan(FIVE_HUNDRED_MILLISECONDS);
global.gc?.();
done();
},
});
});

test('[Ten million items]: executes in less than 1s', (done) => {
expect(TEN_MILLION_ITEMS.length).toBe(10_000_000);

logPerformance({
name: 'Ten Million Items',
fn: () => byAge(TEN_MILLION_ITEMS),
assertion: (actual) => {
expect(actual).toBeLessThan(ONE_SECOND);
global.gc?.();
done();
},
});
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
});
});
5 changes: 3 additions & 2 deletions src/utils/createGroup/createGroup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ const listGroup = [
{ name: 'Woofer', age: 22 },
{ name: 'Dan', age: 20 },
];

describe('createGroup', () => {
it('one', () => {
const byId = createGroup('age');
expect(byId(listGroup)).toEqual({
const byAge = createGroup('age');
expect(byAge(listGroup)).toEqual({
entities: {
'20': [
{
Expand Down
2 changes: 1 addition & 1 deletion src/utils/createGroup/createGroup.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { EntityDict, ObjectKey } from '../../typings';
import { groupBy } from '../_internal';
import { groupBy } from '../__internal__';

export function createGroup<K extends ObjectKey>(key: K) {
return function groupList<T extends Record<K, T[K]>>(
Expand Down

0 comments on commit 6277380

Please sign in to comment.