feat(providence-analytics): enhance and test memoization
This commit is contained in:
parent
d92860a565
commit
dd3458af70
5 changed files with 353 additions and 37 deletions
5
.changeset/pink-games-grin.md
Normal file
5
.changeset/pink-games-grin.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'providence-analytics': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
improve memoization
|
||||||
|
|
@ -7,7 +7,7 @@ const {
|
||||||
const { resolveImportPath } = require('../../utils/resolve-import-path.js');
|
const { resolveImportPath } = require('../../utils/resolve-import-path.js');
|
||||||
const { AstService } = require('../../services/AstService.js');
|
const { AstService } = require('../../services/AstService.js');
|
||||||
const { LogService } = require('../../services/LogService.js');
|
const { LogService } = require('../../services/LogService.js');
|
||||||
const { memoizeAsync } = require('../../utils/memoize.js');
|
const { memoize } = require('../../utils/memoize.js');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../../types/core').RootFile} RootFile
|
* @typedef {import('../../types/core').RootFile} RootFile
|
||||||
|
|
@ -233,7 +233,7 @@ async function trackDownIdentifierFn(source, identifierName, currentFilePath, ro
|
||||||
return /** @type { RootFile } */ { file: rootFilePath, specifier: rootSpecifier };
|
return /** @type { RootFile } */ { file: rootFilePath, specifier: rootSpecifier };
|
||||||
}
|
}
|
||||||
|
|
||||||
trackDownIdentifier = memoizeAsync(trackDownIdentifierFn);
|
trackDownIdentifier = memoize(trackDownIdentifierFn);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {BabelPath} astPath
|
* @param {BabelPath} astPath
|
||||||
|
|
@ -265,7 +265,7 @@ async function trackDownIdentifierFromScopeFn(
|
||||||
return rootFile;
|
return rootFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
const trackDownIdentifierFromScope = memoizeAsync(trackDownIdentifierFromScopeFn);
|
const trackDownIdentifierFromScope = memoize(trackDownIdentifierFromScopeFn);
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
trackDownIdentifier,
|
trackDownIdentifier,
|
||||||
|
|
|
||||||
|
|
@ -1,54 +1,45 @@
|
||||||
const { InputDataService } = require('../services/InputDataService.js');
|
const { InputDataService } = require('../services/InputDataService.js');
|
||||||
|
|
||||||
|
function isObject(arg) {
|
||||||
|
return !Array.isArray(arg) && typeof arg === 'object';
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCachableArg(arg) {
|
||||||
|
if (isObject(arg)) {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(arg);
|
||||||
|
} catch {
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {function} func
|
* @param {function} functionToMemoize
|
||||||
* @param {object} [storage]
|
* @param {{ storage:object; serializeObjects: boolean }} [opts]
|
||||||
*/
|
*/
|
||||||
function memoize(func, storage = {}) {
|
function memoize(functionToMemoize, { storage = {}, serializeObjects = false } = {}) {
|
||||||
// eslint-disable-next-line func-names
|
// eslint-disable-next-line func-names
|
||||||
return function () {
|
return function () {
|
||||||
// eslint-disable-next-line prefer-rest-params
|
// eslint-disable-next-line prefer-rest-params
|
||||||
const args = [...arguments];
|
const args = [...arguments];
|
||||||
|
const cachableArgs = !serializeObjects ? args : args.map(createCachableArg);
|
||||||
// Allow disabling of cache for testing purposes
|
// Allow disabling of cache for testing purposes
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (!InputDataService.cacheDisabled && args in storage) {
|
if (!InputDataService.cacheDisabled && cachableArgs in storage) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return storage[args];
|
return storage[cachableArgs];
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const outcome = func.apply(this, args);
|
const outcome = functionToMemoize.apply(this, args);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
storage[args] = outcome;
|
storage[cachableArgs] = outcome;
|
||||||
return outcome;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {function} func
|
|
||||||
* @param {object} [storage]
|
|
||||||
*/
|
|
||||||
function memoizeAsync(func, storage = {}) {
|
|
||||||
// eslint-disable-next-line func-names
|
|
||||||
return async function () {
|
|
||||||
// eslint-disable-next-line prefer-rest-params
|
|
||||||
const args = [...arguments];
|
|
||||||
// Allow disabling of cache for testing purposes
|
|
||||||
// @ts-ignore
|
|
||||||
if (!InputDataService.cacheDisabled && args in storage) {
|
|
||||||
// @ts-ignore
|
|
||||||
return storage[args];
|
|
||||||
}
|
|
||||||
// @ts-ignore
|
|
||||||
const outcome = await func.apply(this, args);
|
|
||||||
// @ts-ignore
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
storage[args] = outcome;
|
|
||||||
return outcome;
|
return outcome;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
memoize,
|
memoize,
|
||||||
memoizeAsync,
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
const pathLib = require('path');
|
const pathLib = require('path');
|
||||||
const { nodeResolve } = require('@rollup/plugin-node-resolve');
|
const { nodeResolve } = require('@rollup/plugin-node-resolve');
|
||||||
const { LogService } = require('../services/LogService.js');
|
const { LogService } = require('../services/LogService.js');
|
||||||
const { memoizeAsync } = require('./memoize.js');
|
const { memoize } = require('./memoize.js');
|
||||||
const { toPosixPath } = require('./to-posix-path.js');
|
const { toPosixPath } = require('./to-posix-path.js');
|
||||||
|
|
||||||
const fakePluginContext = {
|
const fakePluginContext = {
|
||||||
|
|
@ -64,6 +64,6 @@ async function resolveImportPath(importee, importer, opts = {}) {
|
||||||
* @param {{customResolveOptions?: {preserveSymlinks:boolean}}} [opts] nodeResolve options
|
* @param {{customResolveOptions?: {preserveSymlinks:boolean}}} [opts] nodeResolve options
|
||||||
* @returns {Promise<PathFromSystemRoot|null>} the resolved file system path, like '/my/project/node_modules/@lion/core/index.js'
|
* @returns {Promise<PathFromSystemRoot|null>} the resolved file system path, like '/my/project/node_modules/@lion/core/index.js'
|
||||||
*/
|
*/
|
||||||
const resolveImportPathMemoized = memoizeAsync(resolveImportPath);
|
const resolveImportPathMemoized = memoize(resolveImportPath);
|
||||||
|
|
||||||
module.exports = { resolveImportPath: resolveImportPathMemoized };
|
module.exports = { resolveImportPath: resolveImportPathMemoized };
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,320 @@
|
||||||
|
const { expect } = require('chai');
|
||||||
|
const { memoize } = require('../../../src/program/utils/memoize.js');
|
||||||
|
const { InputDataService } = require('../../../src/program/services/InputDataService.js');
|
||||||
|
|
||||||
|
const cacheDisabledInitialValue = InputDataService.cacheDisabled;
|
||||||
|
|
||||||
|
describe('Memoize', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// This is important, since memoization only works
|
||||||
|
InputDataService.cacheDisabled = false;
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
InputDataService.cacheDisabled = cacheDisabledInitialValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('With primitives', () => {
|
||||||
|
describe('Numbers', () => {
|
||||||
|
it(`returns cached result when called with same parameters`, async () => {
|
||||||
|
let sumCalled = 0;
|
||||||
|
function sum(/** @type {number} a */ a, /** @type {number} a */ b) {
|
||||||
|
sumCalled += 1;
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
const sumMemoized = memoize(sum);
|
||||||
|
|
||||||
|
// Put in cache for args combination
|
||||||
|
expect(sumMemoized(1, 2)).to.equal(3);
|
||||||
|
expect(sumCalled).to.equal(1);
|
||||||
|
|
||||||
|
// Return from cache
|
||||||
|
expect(sumMemoized(1, 2)).to.equal(3);
|
||||||
|
expect(sumCalled).to.equal(1);
|
||||||
|
|
||||||
|
// Put in cache for args combination
|
||||||
|
expect(sumMemoized(1, 3)).to.equal(4);
|
||||||
|
expect(sumCalled).to.equal(2);
|
||||||
|
|
||||||
|
// Return from cache
|
||||||
|
expect(sumMemoized(1, 3)).to.equal(4);
|
||||||
|
expect(sumCalled).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`returns cached result per function for same args`, async () => {
|
||||||
|
let sumCalled = 0;
|
||||||
|
function sum(/** @type {number} a */ a, /** @type {number} a */ b) {
|
||||||
|
sumCalled += 1;
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
const sumMemoized = memoize(sum);
|
||||||
|
let sum2Called = 0;
|
||||||
|
function sum2(/** @type {number} a */ a, /** @type {number} a */ b) {
|
||||||
|
sum2Called += 1;
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
const sum2Memoized = memoize(sum2);
|
||||||
|
|
||||||
|
expect(sumMemoized(1, 2)).to.equal(3);
|
||||||
|
expect(sumCalled).to.equal(1);
|
||||||
|
expect(sum2Called).to.equal(0);
|
||||||
|
|
||||||
|
expect(sum2Memoized(1, 2)).to.equal(3);
|
||||||
|
expect(sumCalled).to.equal(1);
|
||||||
|
expect(sum2Called).to.equal(1);
|
||||||
|
|
||||||
|
// Both cached
|
||||||
|
expect(sumMemoized(1, 2)).to.equal(3);
|
||||||
|
expect(sum2Memoized(1, 2)).to.equal(3);
|
||||||
|
expect(sumCalled).to.equal(1);
|
||||||
|
expect(sum2Called).to.equal(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Strings', () => {
|
||||||
|
it(`returns cached result when called with same parameters`, async () => {
|
||||||
|
let sumCalled = 0;
|
||||||
|
function sum(/** @type {string} a */ a, /** @type {string} a */ b) {
|
||||||
|
sumCalled += 1;
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
const sumMemoized = memoize(sum);
|
||||||
|
|
||||||
|
// Put in cache for args combination
|
||||||
|
expect(sumMemoized('1', '2')).to.equal('12');
|
||||||
|
expect(sumCalled).to.equal(1);
|
||||||
|
|
||||||
|
// Return from cache
|
||||||
|
expect(sumMemoized('1', '2')).to.equal('12');
|
||||||
|
expect(sumCalled).to.equal(1);
|
||||||
|
|
||||||
|
// Put in cache for args combination
|
||||||
|
expect(sumMemoized('1', '3')).to.equal('13');
|
||||||
|
expect(sumCalled).to.equal(2);
|
||||||
|
|
||||||
|
// Return from cache
|
||||||
|
expect(sumMemoized('1', '3')).to.equal('13');
|
||||||
|
expect(sumCalled).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`returns cached result per function for same args`, async () => {
|
||||||
|
let sumCalled = 0;
|
||||||
|
function sum(/** @type {string} a */ a, /** @type {string} a */ b) {
|
||||||
|
sumCalled += 1;
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
const sumMemoized = memoize(sum);
|
||||||
|
let sum2Called = 0;
|
||||||
|
function sum2(/** @type {string} a */ a, /** @type {string} a */ b) {
|
||||||
|
sum2Called += 1;
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
const sum2Memoized = memoize(sum2);
|
||||||
|
|
||||||
|
expect(sumMemoized('1', '2')).to.equal('12');
|
||||||
|
expect(sumCalled).to.equal(1);
|
||||||
|
expect(sum2Called).to.equal(0);
|
||||||
|
|
||||||
|
expect(sum2Memoized('1', '2')).to.equal('12');
|
||||||
|
expect(sumCalled).to.equal(1);
|
||||||
|
expect(sum2Called).to.equal(1);
|
||||||
|
|
||||||
|
// Both cached
|
||||||
|
expect(sumMemoized('1', '2')).to.equal('12');
|
||||||
|
expect(sum2Memoized('1', '2')).to.equal('12');
|
||||||
|
expect(sumCalled).to.equal(1);
|
||||||
|
expect(sum2Called).to.equal(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('With non primitives', () => {
|
||||||
|
describe('Arrays', () => {
|
||||||
|
it(`returns cached result when called with same parameters`, async () => {
|
||||||
|
let sumCalled = 0;
|
||||||
|
function sum(/** @type {number[]} a */ a, /** @type {number[]} a */ b) {
|
||||||
|
sumCalled += 1;
|
||||||
|
return [...a, ...b];
|
||||||
|
}
|
||||||
|
const sumMemoized = memoize(sum);
|
||||||
|
|
||||||
|
// Put in cache for args combination
|
||||||
|
expect(sumMemoized([1], [2])).to.eql([1, 2]);
|
||||||
|
expect(sumCalled).to.equal(1);
|
||||||
|
|
||||||
|
// Return from cache
|
||||||
|
expect(sumMemoized([1], [2])).to.eql([1, 2]);
|
||||||
|
expect(sumCalled).to.equal(1);
|
||||||
|
|
||||||
|
// Put in cache for args combination
|
||||||
|
expect(sumMemoized([1], [3])).to.eql([1, 3]);
|
||||||
|
expect(sumCalled).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`returns cached result per function for same args`, async () => {
|
||||||
|
let sumCalled = 0;
|
||||||
|
function sum(/** @type {number[]} a */ a, /** @type {number[]} a */ b) {
|
||||||
|
sumCalled += 1;
|
||||||
|
return [...a, ...b];
|
||||||
|
}
|
||||||
|
const sumMemoized = memoize(sum);
|
||||||
|
let sum2Called = 0;
|
||||||
|
function sum2(/** @type {number[]} a */ a, /** @type {number[]} a */ b) {
|
||||||
|
sum2Called += 1;
|
||||||
|
return [...a, ...b];
|
||||||
|
}
|
||||||
|
const sum2Memoized = memoize(sum2);
|
||||||
|
|
||||||
|
expect(sumMemoized([1], [2])).to.eql([1, 2]);
|
||||||
|
expect(sumCalled).to.equal(1);
|
||||||
|
expect(sum2Called).to.equal(0);
|
||||||
|
|
||||||
|
expect(sum2Memoized([1], [2])).to.eql([1, 2]);
|
||||||
|
expect(sumCalled).to.equal(1);
|
||||||
|
expect(sum2Called).to.equal(1);
|
||||||
|
|
||||||
|
// Both cached
|
||||||
|
expect(sumMemoized([1], [2])).to.eql([1, 2]);
|
||||||
|
expect(sum2Memoized([1], [2])).to.eql([1, 2]);
|
||||||
|
expect(sumCalled).to.equal(1);
|
||||||
|
expect(sum2Called).to.equal(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Objects', () => {
|
||||||
|
it(`returns cached result when called with same parameters`, async () => {
|
||||||
|
let sumCalled = 0;
|
||||||
|
function sum(/** @type {object} a */ a, /** @type {object} a */ b) {
|
||||||
|
sumCalled += 1;
|
||||||
|
return { ...a, ...b };
|
||||||
|
}
|
||||||
|
const sumMemoized = memoize(sum, { serializeObjects: true });
|
||||||
|
|
||||||
|
// Put in cache for args combination
|
||||||
|
expect(sumMemoized({ x: 1 }, { y: 2 })).to.eql({ x: 1, y: 2 });
|
||||||
|
expect(sumCalled).to.equal(1);
|
||||||
|
|
||||||
|
// Return from cache
|
||||||
|
expect(sumMemoized({ x: 1 }, { y: 2 })).to.eql({ x: 1, y: 2 });
|
||||||
|
expect(sumCalled).to.equal(1);
|
||||||
|
|
||||||
|
// Put in cache for args combination
|
||||||
|
expect(sumMemoized({ x: 1 }, { y: 3 })).to.eql({ x: 1, y: 3 });
|
||||||
|
expect(sumCalled).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`returns cached result per function for same args`, async () => {
|
||||||
|
let sumCalled = 0;
|
||||||
|
function sum(/** @type {object} a */ a, /** @type {object} a */ b) {
|
||||||
|
sumCalled += 1;
|
||||||
|
return { ...a, ...b };
|
||||||
|
}
|
||||||
|
const sumMemoized = memoize(sum, { serializeObjects: true });
|
||||||
|
let sum2Called = 0;
|
||||||
|
function sum2(/** @type {object} a */ a, /** @type {object} a */ b) {
|
||||||
|
sum2Called += 1;
|
||||||
|
return { ...a, ...b };
|
||||||
|
}
|
||||||
|
const sum2Memoized = memoize(sum2, { serializeObjects: true });
|
||||||
|
|
||||||
|
expect(sumMemoized({ x: 1 }, { y: 2 })).to.eql({ x: 1, y: 2 });
|
||||||
|
expect(sumCalled).to.equal(1);
|
||||||
|
expect(sum2Called).to.equal(0);
|
||||||
|
|
||||||
|
expect(sum2Memoized({ x: 1 }, { y: 2 })).to.eql({ x: 1, y: 2 });
|
||||||
|
expect(sumCalled).to.equal(1);
|
||||||
|
expect(sum2Called).to.equal(1);
|
||||||
|
|
||||||
|
// Both cached
|
||||||
|
expect(sumMemoized({ x: 1 }, { y: 2 })).to.eql({ x: 1, y: 2 });
|
||||||
|
expect(sum2Memoized({ x: 1 }, { y: 2 })).to.eql({ x: 1, y: 2 });
|
||||||
|
expect(sumCalled).to.equal(1);
|
||||||
|
expect(sum2Called).to.equal(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('When non primitives (references) are returned', () => {
|
||||||
|
// Solve this by making sure your memoized function uses Object.freeze
|
||||||
|
it(`will be affected by edited non primitive returns`, async () => {
|
||||||
|
let sumCalled = 0;
|
||||||
|
function sum(/** @type {object} a */ a, /** @type {object} a */ b) {
|
||||||
|
sumCalled += 1;
|
||||||
|
return { ...a, ...b };
|
||||||
|
}
|
||||||
|
const sumMemoized = memoize(sum, { serializeObjects: true });
|
||||||
|
|
||||||
|
// Put in cache for args combination
|
||||||
|
const result = sumMemoized({ x: 1 }, { y: 2 });
|
||||||
|
expect(result).to.eql({ x: 1, y: 2 });
|
||||||
|
expect(sumCalled).to.equal(1);
|
||||||
|
|
||||||
|
// Return from cache
|
||||||
|
const resultCached = sumMemoized({ x: 1 }, { y: 2 });
|
||||||
|
expect(resultCached).to.equal(result);
|
||||||
|
expect(resultCached).to.eql({ x: 1, y: 2 });
|
||||||
|
expect(sumCalled).to.equal(1);
|
||||||
|
|
||||||
|
// Outside world can edit returned reference
|
||||||
|
resultCached.x = 3;
|
||||||
|
// Return from cache
|
||||||
|
const lastResult = sumMemoized({ x: 1 }, { y: 2 });
|
||||||
|
expect(lastResult).to.equal(result);
|
||||||
|
expect(lastResult).to.not.eql({ x: 1, y: 2 });
|
||||||
|
expect(sumCalled).to.equal(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Asynchronous', () => {
|
||||||
|
it(`returns cached result when called with same parameters`, async () => {
|
||||||
|
let sumCalled = 0;
|
||||||
|
async function sum(/** @type {number} a */ a, /** @type {number} a */ b) {
|
||||||
|
sumCalled += 1;
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
const sumMemoized = memoize(sum);
|
||||||
|
|
||||||
|
// Put in cache for args combination
|
||||||
|
expect(await sumMemoized(1, 2)).to.equal(3);
|
||||||
|
expect(sumCalled).to.equal(1);
|
||||||
|
|
||||||
|
// Return from cache
|
||||||
|
expect(await sumMemoized(1, 2)).to.equal(3);
|
||||||
|
expect(sumCalled).to.equal(1);
|
||||||
|
|
||||||
|
// Put in cache for args combination
|
||||||
|
expect(await sumMemoized(1, 3)).to.equal(4);
|
||||||
|
expect(sumCalled).to.equal(2);
|
||||||
|
|
||||||
|
// Return from cache
|
||||||
|
expect(await sumMemoized(1, 3)).to.equal(4);
|
||||||
|
expect(sumCalled).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`returns cached result per function for same args`, async () => {
|
||||||
|
let sumCalled = 0;
|
||||||
|
async function sum(/** @type {number} a */ a, /** @type {number} a */ b) {
|
||||||
|
sumCalled += 1;
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
const sumMemoized = memoize(sum);
|
||||||
|
let sum2Called = 0;
|
||||||
|
async function sum2(/** @type {number} a */ a, /** @type {number} a */ b) {
|
||||||
|
sum2Called += 1;
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
const sum2Memoized = memoize(sum2);
|
||||||
|
|
||||||
|
expect(await sumMemoized(1, 2)).to.equal(3);
|
||||||
|
expect(sumCalled).to.equal(1);
|
||||||
|
expect(sum2Called).to.equal(0);
|
||||||
|
|
||||||
|
expect(await sum2Memoized(1, 2)).to.equal(3);
|
||||||
|
expect(sumCalled).to.equal(1);
|
||||||
|
expect(sum2Called).to.equal(1);
|
||||||
|
|
||||||
|
// Both cached
|
||||||
|
expect(await sumMemoized(1, 2)).to.equal(3);
|
||||||
|
expect(await sum2Memoized(1, 2)).to.equal(3);
|
||||||
|
expect(sumCalled).to.equal(1);
|
||||||
|
expect(sum2Called).to.equal(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue