From 2daa01d6f6cce6a9d2ad5507b4c12e499426c443 Mon Sep 17 00:00:00 2001 From: TheoryOfNekomata Date: Sat, 8 Aug 2020 18:27:29 +0800 Subject: [PATCH] Improve performance Add memoization to expensive pure functions because the library involves a lot of computation under the hood. --- package.json | 4 +- src/components/Keyboard/Keyboard.tsx | 11 ++++-- .../{constants/keyOffsets.ts => constants.ts} | 6 ++- src/services/generateKeys.ts | 4 +- src/services/getFractionalOctaveCount.ts | 28 ++++++++++++++ src/services/getKeyLeft.ts | 34 +++++++---------- src/services/getKeyOctave.ts | 7 ++++ src/services/getKeyWidth.ts | 38 ++++++++----------- src/services/getKeyXOffset.ts | 10 ++++- src/services/getOctaveCompleteness.ts | 16 ++++++-- src/services/getOctaveCount.ts | 11 +++--- src/services/groupKeysIntoOctaves.ts | 8 +++- src/services/isNaturalKey.ts | 8 +++- yarn.lock | 37 ++++++++++++++++++ 14 files changed, 159 insertions(+), 63 deletions(-) rename src/services/{constants/keyOffsets.ts => constants.ts} (92%) create mode 100644 src/services/getFractionalOctaveCount.ts create mode 100644 src/services/getKeyOctave.ts diff --git a/package.json b/package.json index 20afd24..55409d3 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "build-storybook": "build-storybook" }, "dependencies": { - "styled-components": "^5.1.1" + "mem": "^6.1.0", + "styled-components": "^5.1.1", + "typescript-memoize": "^1.0.0-alpha.3" }, "peerDependencies": { "react": ">=16" diff --git a/src/components/Keyboard/Keyboard.tsx b/src/components/Keyboard/Keyboard.tsx index 14adbd8..c012a9f 100644 --- a/src/components/Keyboard/Keyboard.tsx +++ b/src/components/Keyboard/Keyboard.tsx @@ -1,9 +1,10 @@ import * as React from 'react' import * as PropTypes from 'prop-types' import styled from 'styled-components' -import isNaturalKey from '../../services/isNaturalKey' -import getKeyWidth from '../../services/getKeyWidth' -import getKeyLeft from '../../services/getKeyLeft' +import mem from 'mem' +import isNaturalKeyUnmemoized from '../../services/isNaturalKey' +import getKeyWidthUnmemoized from '../../services/getKeyWidth' +import getKeyLeftUnmemoized from '../../services/getKeyLeft' import generateKeys from '../../services/generateKeys' import * as DefaultAccidentalKey from '../AccidentalKey/AccidentalKey' import * as DefaultNaturalKey from '../NaturalKey/NaturalKey' @@ -19,6 +20,10 @@ const Key = styled('div')({ top: 0, }) +const getKeyWidth = mem(getKeyWidthUnmemoized, { cacheKey: (args) => args.join(':') }) +const getKeyLeft = mem(getKeyLeftUnmemoized, { cacheKey: (args) => args.join(':') }) +const isNaturalKey = mem(isNaturalKeyUnmemoized) + export const propTypes = { /** * MIDI note of the first key. diff --git a/src/services/constants/keyOffsets.ts b/src/services/constants.ts similarity index 92% rename from src/services/constants/keyOffsets.ts rename to src/services/constants.ts index c6d7f37..1969690 100644 --- a/src/services/constants/keyOffsets.ts +++ b/src/services/constants.ts @@ -20,7 +20,7 @@ */ -// export default [ +// export const KEY_OFFSETS = [ // 0, // C // 3 / 7 / 5, // C# // 1 / 7, // D @@ -36,7 +36,7 @@ // ] // http://datagenetics.com/blog/may32016/index.html -export default [ +export const KEY_OFFSETS = [ 0, // C 525 / 5880, // C# 1 / 7, // D @@ -50,3 +50,5 @@ export default [ (525 + 490 * 7 + 525 + 455) / 5880, // A# 6 / 7, // B ] + +export const ACCIDENTAL_KEY_TO_NATURAL_KEY_WIDTH_RATIO = 13 / 23 diff --git a/src/services/generateKeys.ts b/src/services/generateKeys.ts index 231558b..842c2a7 100644 --- a/src/services/generateKeys.ts +++ b/src/services/generateKeys.ts @@ -3,8 +3,8 @@ interface GenerateKeys { } const generateKeys: GenerateKeys = (startKey, endKey) => - Array(endKey! - startKey! + 1) + Array(endKey - startKey + 1) .fill(0) - .map((_, i) => startKey! + i) + .map((_, i) => startKey + i) export default generateKeys diff --git a/src/services/getFractionalOctaveCount.ts b/src/services/getFractionalOctaveCount.ts new file mode 100644 index 0000000..849872e --- /dev/null +++ b/src/services/getFractionalOctaveCount.ts @@ -0,0 +1,28 @@ +import mem from 'mem' +import generateKeys from './generateKeys' +import groupKeysIntoOctaves from './groupKeysIntoOctaves' +import getOctaveCompletenessUnmemoized from './getOctaveCompleteness' + +const getOctaveCompleteness = mem(getOctaveCompletenessUnmemoized) + +interface GetFractionalOctaveCount { + (startKey: number, endKey: number): number +} + +const getFractionalOctaveCount: GetFractionalOctaveCount = (startKey, endKey) => { + const dummyKeys = generateKeys(startKey, endKey) + const keysGroupedIntoOctaves = groupKeysIntoOctaves(dummyKeys) + const octaveCompleteness = Object.entries(keysGroupedIntoOctaves) + .map(([octave, keys]) => [(octave as unknown) as number, keys[0], keys.slice(-1)[0]]) + .reduce>( + (theOctaveCompleteness, [octave, firstKey, lastKey]) => ({ + ...theOctaveCompleteness, + [octave]: getOctaveCompleteness(firstKey, lastKey), + }), + {}, + ) + + return Object.values(octaveCompleteness).reduce((a, b) => a + b, 0) +} + +export default getFractionalOctaveCount diff --git a/src/services/getKeyLeft.ts b/src/services/getKeyLeft.ts index 2e99b73..d499345 100644 --- a/src/services/getKeyLeft.ts +++ b/src/services/getKeyLeft.ts @@ -1,8 +1,13 @@ -import getKeyXOffset from './getKeyXOffset' -import getOctaveCount from './getOctaveCount' -import generateKeys from './generateKeys' -import groupKeysIntoOctaves from './groupKeysIntoOctaves' -import getOctaveCompleteness from './getOctaveCompleteness' +import mem from 'mem' +import getKeyXOffsetUnmemoized from './getKeyXOffset' +import getOctaveCountUnmemoized from './getOctaveCount' +import getFractionalOctaveCountUnmemoized from './getFractionalOctaveCount' +import getKeyOctaveUnmemoized from './getKeyOctave' + +const getKeyXOffset = mem(getKeyXOffsetUnmemoized) +const getOctaveCount = mem(getOctaveCountUnmemoized, { cacheKey: (args) => args.join(':') }) +const getFractionalOctaveCount = mem(getFractionalOctaveCountUnmemoized, { cacheKey: (args) => args.join(':') }) +const getKeyOctave = mem(getKeyOctaveUnmemoized) interface GetKeyLeft { (k: number): number @@ -13,23 +18,10 @@ interface GetKeyLeftDecorator { } const getKeyLeftDecorator: GetKeyLeftDecorator = (startKey, endKey): GetKeyLeft => (k) => { - const dummyKeys = generateKeys(startKey, endKey) - const keysGroupedIntoOctaves = groupKeysIntoOctaves(dummyKeys) - const octaveCompleteness = Object.entries(keysGroupedIntoOctaves) - .map(([octave, keys]) => [(octave as unknown) as number, keys[0], keys.slice(-1)[0]]) - .reduce>( - (theOctaveCompleteness, [octave, firstKey, lastKey]) => ({ - ...theOctaveCompleteness, - [octave]: getOctaveCompleteness(firstKey, lastKey), - }), - {}, - ) - - const fractionalOctaveCount = Object.values(octaveCompleteness).reduce((a, b) => a + b, 0) + const fractionalOctaveCount = getFractionalOctaveCount(startKey, endKey) const octaveCount = getOctaveCount(startKey, endKey) - - const startOctave = Math.floor(startKey! / 12) - const octave = Math.floor(k / 12) + const startOctave = getKeyOctave(startKey) + const octave = getKeyOctave(k) const octaveOffset = ((100 * octaveCount) / fractionalOctaveCount / octaveCount) * (octave - startOctave) const theKeyOffset = octaveOffset + ((100 * octaveCount) / fractionalOctaveCount / octaveCount) * getKeyXOffset(k) const firstKeyOffset = ((100 * octaveCount) / fractionalOctaveCount / octaveCount) * getKeyXOffset(startKey + 12) diff --git a/src/services/getKeyOctave.ts b/src/services/getKeyOctave.ts new file mode 100644 index 0000000..8222caf --- /dev/null +++ b/src/services/getKeyOctave.ts @@ -0,0 +1,7 @@ +interface GetKeyOctave { + (k: number): number +} + +const getKeyOctave: GetKeyOctave = (k) => Math.floor(k / 12) + +export default getKeyOctave diff --git a/src/services/getKeyWidth.ts b/src/services/getKeyWidth.ts index 6d8b8ec..3ef1d38 100644 --- a/src/services/getKeyWidth.ts +++ b/src/services/getKeyWidth.ts @@ -1,8 +1,8 @@ -import isNaturalKey from './isNaturalKey' -import groupKeysIntoOctaves from './groupKeysIntoOctaves' -import getOctaveCompleteness from './getOctaveCompleteness' -import getOctaveCount from './getOctaveCount' -import generateKeys from './generateKeys' +import mem from 'mem' +import isNaturalKeyUnmemoized from './isNaturalKey' +import getOctaveCountUnmemoized from './getOctaveCount' +import getFractionalOctaveCountUnmemoized from './getFractionalOctaveCount' +import { ACCIDENTAL_KEY_TO_NATURAL_KEY_WIDTH_RATIO } from './constants' interface GetKeyWidth { (k: number): number @@ -12,25 +12,19 @@ interface GetKeyWidthDecorator { (startKey: number, endKey: number): GetKeyWidth } -const ACCIDENTAL_KEY_TO_NATURAL_KEY_WIDTH_RATIO = 13 / 23 +const isNaturalKey = mem(isNaturalKeyUnmemoized) +const getFractionalOctaveCount = mem(getFractionalOctaveCountUnmemoized, { cacheKey: (args) => args.join(':') }) +const getOctaveCount = mem(getOctaveCountUnmemoized, { cacheKey: (args) => args.join(':') }) -const getKeyWidthDecorator: GetKeyWidthDecorator = (startKey, endKey): GetKeyWidth => (k) => { - const dummyKeys = generateKeys(startKey, endKey) - const keysGroupedIntoOctaves = groupKeysIntoOctaves(dummyKeys) - const octaveCompleteness = Object.entries(keysGroupedIntoOctaves) - .map(([octave, keys]) => [(octave as unknown) as number, keys[0], keys.slice(-1)[0]]) - .reduce>( - (theOctaveCompleteness, [octave, firstKey, lastKey]) => ({ - ...theOctaveCompleteness, - [octave]: getOctaveCompleteness(firstKey, lastKey), - }), - {}, - ) +const getKeyWidthDecorator: GetKeyWidthDecorator = (startKey, endKey) => { + const getKeyWidth: GetKeyWidth = (k) => { + const fractionalOctaveCount = getFractionalOctaveCount(startKey, endKey) + const octaveCount = getOctaveCount(startKey, endKey) + const naturalKeyWidth = (100 * (octaveCount / fractionalOctaveCount)) / (octaveCount * 7) + return isNaturalKey(k) ? naturalKeyWidth : naturalKeyWidth * ACCIDENTAL_KEY_TO_NATURAL_KEY_WIDTH_RATIO + } - const fractionalOctaveCount = Object.values(octaveCompleteness).reduce((a, b) => a + b, 0) - const octaveCount = getOctaveCount(startKey, endKey) - const naturalKeyWidth = (100 * (octaveCount / fractionalOctaveCount)) / (octaveCount * 7) - return isNaturalKey(k) ? naturalKeyWidth : naturalKeyWidth * ACCIDENTAL_KEY_TO_NATURAL_KEY_WIDTH_RATIO + return mem(getKeyWidth) } export default getKeyWidthDecorator diff --git a/src/services/getKeyXOffset.ts b/src/services/getKeyXOffset.ts index 1e2264c..2dea681 100644 --- a/src/services/getKeyXOffset.ts +++ b/src/services/getKeyXOffset.ts @@ -1,5 +1,11 @@ -import KEY_OFFSETS from './constants/keyOffsets' +import { KEY_OFFSETS } from './constants' -export default (k: number): number => { +interface GetKeyXOffset { + (k: number): number +} + +const getKeyXOffset: GetKeyXOffset = (k) => { return KEY_OFFSETS[k % 12] } + +export default getKeyXOffset diff --git a/src/services/getOctaveCompleteness.ts b/src/services/getOctaveCompleteness.ts index 51852a5..af1b393 100644 --- a/src/services/getOctaveCompleteness.ts +++ b/src/services/getOctaveCompleteness.ts @@ -1,10 +1,20 @@ -import getKeyXOffset from './getKeyXOffset' -import isNaturalKey from './isNaturalKey' +import mem from 'mem' +import getKeyXOffsetUnmemoized from './getKeyXOffset' +import isNaturalKeyUnmemoized from './isNaturalKey' + +const getKeyXOffset = mem(getKeyXOffsetUnmemoized) +const isNaturalKey = mem(isNaturalKeyUnmemoized) + +interface GetOctaveCompleteness { + (firstKey: number, lastKey: number): number +} // expect firstKey and lastKey within the same octave -export default (firstKey: number, lastKey: number) => +const getOctaveCompleteness: GetOctaveCompleteness = (firstKey, lastKey) => // see if there are missing higher notes getKeyXOffset(lastKey) + (isNaturalKey(lastKey) ? 1 / 7 : ((1 / 7) * 18) / 36) - // see if there are missing lower notes getKeyXOffset(firstKey) + +export default getOctaveCompleteness diff --git a/src/services/getOctaveCount.ts b/src/services/getOctaveCount.ts index 1658661..a743d6f 100644 --- a/src/services/getOctaveCount.ts +++ b/src/services/getOctaveCount.ts @@ -1,11 +1,12 @@ +import mem from 'mem' +import getKeyOctaveUnmemoized from './getKeyOctave' + +const getKeyOctave = mem(getKeyOctaveUnmemoized) + interface GetOctaveCount { (startKey: number, endKey: number): number } -const getOctaveCount: GetOctaveCount = (startKey, endKey) => { - const startOctave = Math.floor(startKey / 12) - const endOctave = Math.floor(endKey / 12) - return endOctave - startOctave + 1 -} +const getOctaveCount: GetOctaveCount = (startKey, endKey) => getKeyOctave(endKey) - getKeyOctave(startKey) + 1 export default getOctaveCount diff --git a/src/services/groupKeysIntoOctaves.ts b/src/services/groupKeysIntoOctaves.ts index 0cf627b..4c6bc0e 100644 --- a/src/services/groupKeysIntoOctaves.ts +++ b/src/services/groupKeysIntoOctaves.ts @@ -1,4 +1,8 @@ -export default (dummyKeys: number[]): Record => +interface GroupKeysIntoOctaves { + (dummyKeys: number[]): Record +} + +const groupKeysIntoOctaves: GroupKeysIntoOctaves = (dummyKeys) => dummyKeys .map((k) => [k, Math.floor(k / 12)]) .reduce>( @@ -8,3 +12,5 @@ export default (dummyKeys: number[]): Record => }), {}, ) + +export default groupKeysIntoOctaves diff --git a/src/services/isNaturalKey.ts b/src/services/isNaturalKey.ts index cc68067..ad50c45 100644 --- a/src/services/isNaturalKey.ts +++ b/src/services/isNaturalKey.ts @@ -1,6 +1,10 @@ const NATURAL_KEYS = [0, 2, 4, 5, 7, 9, 11] -export default (k: number): boolean => { +interface IsNaturalKey { + (k: number): boolean +} + +const isNaturalKey: IsNaturalKey = (k: number): boolean => { const type = typeof (k as unknown) if ((type as string) !== 'number') { throw TypeError(`Invalid value type passed to isNaturalKey, expected 'number', got ${type}.`) @@ -13,3 +17,5 @@ export default (k: number): boolean => { } return NATURAL_KEYS.includes(Math.floor(k) % 12) } + +export default isNaturalKey diff --git a/yarn.lock b/yarn.lock index df32997..a4a1630 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4249,6 +4249,11 @@ core-js-pure@^3.0.0, core-js-pure@^3.0.1: resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813" integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA== +core-js@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" + integrity sha1-TekR5mew6ukSTjQlS1OupvxhjT4= + core-js@^1.0.0: version "1.2.7" resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" @@ -8027,6 +8032,13 @@ makeerror@1.0.x: dependencies: tmpl "1.0.x" +map-age-cleaner@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" + integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== + dependencies: + p-defer "^1.0.0" + map-cache@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" @@ -8143,6 +8155,14 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= +mem@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-6.1.0.tgz#846eca0bd4708a8f04b9c3f3cd769e194ae63c5c" + integrity sha512-RlbnLQgRHk5lwqTtpEkBTQ2ll/CG/iB+J4Hy2Wh97PjgZgXgWJWrFF+XXujh3UUVLvR4OOTgZzcWMMwnehlEUg== + dependencies: + map-age-cleaner "^0.1.3" + mimic-fn "^3.0.0" + memoizerific@^1.11.3: version "1.11.3" resolved "https://registry.yarnpkg.com/memoizerific/-/memoizerific-1.11.3.tgz#7c87a4646444c32d75438570905f2dbd1b1a805a" @@ -8267,6 +8287,11 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +mimic-fn@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-3.1.0.tgz#65755145bbf3e36954b949c16450427451d5ca74" + integrity sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ== + min-document@^2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" @@ -8832,6 +8857,11 @@ os-tmpdir@~1.0.2: resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= +p-defer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" + integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= + p-each-series@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-1.0.0.tgz#930f3d12dd1f50e7434457a22cd6f04ac6ad7f71" @@ -11740,6 +11770,13 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typescript-memoize@^1.0.0-alpha.3: + version "1.0.0-alpha.3" + resolved "https://registry.yarnpkg.com/typescript-memoize/-/typescript-memoize-1.0.0-alpha.3.tgz#699a5415f886694a8d6e2e5451bc28a39a6bc2f9" + integrity sha1-aZpUFfiGaUqNbi5UUbwoo5prwvk= + dependencies: + core-js "2.4.1" + typescript@^3.7.3, typescript@^3.9.7: version "3.9.7" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa"