Add sequentialize to prevent parralel loading of cipher keys (#7)

* Add sequentialize to prevent parralel loading of cipher keys

Massively improves start up performance of extensions

* Add tests for sequentialize

* Fix sequentialize as it was caching calls for all instances together

* Add sequentialize to the functions that have internal caches

* Adding sequentialize to getOrgKeys makes big performance difference

* Update cipher.service.ts

* Update collection.service.ts

* Update folder.service.ts
This commit is contained in:
Fred Cox 2018-07-23 21:23:30 +03:00 committed by Kyle Spearrin
parent 3a34d3b174
commit 04014a8e78
4 changed files with 197 additions and 0 deletions

View File

@ -0,0 +1,141 @@
import { sequentialize } from '../../../src/misc/sequentialize';
describe('sequentialize decorator', () => {
it('should call the function once', async () => {
const foo = new Foo();
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(foo.bar(1));
}
await Promise.all(promises);
expect(foo.calls).toBe(1);
});
it('should call the function once for each instance of the object', async () => {
const foo = new Foo();
const foo2 = new Foo();
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(foo.bar(1));
promises.push(foo2.bar(1));
}
await Promise.all(promises);
expect(foo.calls).toBe(1);
expect(foo2.calls).toBe(1);
});
it('should call the function once with key function', async () => {
const foo = new Foo();
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(foo.baz(1));
}
await Promise.all(promises);
expect(foo.calls).toBe(1);
});
it('should call the function again when already resolved', async () => {
const foo = new Foo();
await foo.bar(1);
expect(foo.calls).toBe(1);
await foo.bar(1);
expect(foo.calls).toBe(2);
});
it('should call the function again when already resolved with a key function', async () => {
const foo = new Foo();
await foo.baz(1);
expect(foo.calls).toBe(1);
await foo.baz(1);
expect(foo.calls).toBe(2);
});
it('should call the function for each argument', async () => {
const foo = new Foo();
await Promise.all([
foo.bar(1),
foo.bar(1),
foo.bar(2),
foo.bar(2),
foo.bar(3),
foo.bar(3),
]);
expect(foo.calls).toBe(3);
});
it('should call the function for each argument with key function', async () => {
const foo = new Foo();
await Promise.all([
foo.baz(1),
foo.baz(1),
foo.baz(2),
foo.baz(2),
foo.baz(3),
foo.baz(3),
]);
expect(foo.calls).toBe(3);
});
it('should return correct result for each call', async () => {
const foo = new Foo();
const allRes = [];
await Promise.all([
foo.bar(1).then((res) => allRes.push(res)),
foo.bar(1).then((res) => allRes.push(res)),
foo.bar(2).then((res) => allRes.push(res)),
foo.bar(2).then((res) => allRes.push(res)),
foo.bar(3).then((res) => allRes.push(res)),
foo.bar(3).then((res) => allRes.push(res)),
]);
expect(foo.calls).toBe(3);
expect(allRes.length).toBe(6);
allRes.sort();
expect(allRes).toEqual([2, 2, 4, 4, 6, 6]);
});
it('should return correct result for each call with key function', async () => {
const foo = new Foo();
const allRes = [];
await Promise.all([
foo.baz(1).then((res) => allRes.push(res)),
foo.baz(1).then((res) => allRes.push(res)),
foo.baz(2).then((res) => allRes.push(res)),
foo.baz(2).then((res) => allRes.push(res)),
foo.baz(3).then((res) => allRes.push(res)),
foo.baz(3).then((res) => allRes.push(res)),
]);
expect(foo.calls).toBe(3);
expect(allRes.length).toBe(6);
allRes.sort();
expect(allRes).toEqual([3, 3, 6, 6, 9, 9]);
});
});
class Foo {
calls = 0;
@sequentialize()
bar(a) {
this.calls++;
return new Promise((res) => {
setTimeout(() => {
res(a * 2);
}, Math.random() * 100);
});
}
@sequentialize((args) => args[0])
baz(a) {
this.calls++;
return new Promise((res) => {
setTimeout(() => {
res(a * 3);
}, Math.random() * 100);
});
}
}

52
src/misc/sequentialize.ts Normal file
View File

@ -0,0 +1,52 @@
/**
* Use as a Decorator on async functions, it will prevent multiple 'active' calls as the same time
*
* If a promise was returned from a previous call to this function, that hasn't yet resolved it will
* be returned, instead of calling the original function again
*
* Results are not cached, once the promise has returned, the next call will result in a fresh call
*/
export function sequentialize(key: (args: any[]) => string = JSON.stringify) {
return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
const originalMethod: () => Promise<any> = descriptor.value;
const caches = new Map<any, Map<string, Promise<any>>>();
const getCache = (obj: any) => {
let cache = caches.get(obj);
if (cache) {
return cache;
}
cache = new Map<string, Promise<any>>();
caches.set(obj, cache);
return cache;
};
return {
value: function(...args: any[]) {
const argsKey = key(args);
const cache = getCache(this);
let res = cache.get(argsKey);
if (res) {
return res;
}
res = originalMethod.apply(this, args)
.then((val: any) => {
cache.delete(argsKey);
return val;
})
.catch((err: any) => {
cache.delete(argsKey);
throw err;
});
cache.set(argsKey, res);
return res;
},
};
};
}

View File

@ -2,6 +2,7 @@ import { EncryptionType } from '../../enums/encryptionType';
import { CryptoService } from '../../abstractions/crypto.service';
import { sequentialize } from '../../misc/sequentialize';
import { Utils } from '../../misc/utils';
export class CipherString {
@ -89,6 +90,7 @@ export class CipherString {
}
}
@sequentialize((args) => args[0])
async decrypt(orgId: string): Promise<string> {
if (this.decryptedValue) {
return Promise.resolve(this.decryptedValue);

View File

@ -11,6 +11,7 @@ import { StorageService } from '../abstractions/storage.service';
import { ConstantsService } from './constants.service';
import { sequentialize } from '../misc/sequentialize';
import { Utils } from '../misc/utils';
const Keys = {
@ -164,6 +165,7 @@ export class CryptoService implements CryptoServiceAbstraction {
return this.privateKey;
}
@sequentialize()
async getOrgKeys(): Promise<Map<string, SymmetricCryptoKey>> {
if (this.orgKeys != null && this.orgKeys.size > 0) {
return this.orgKeys;