mirror of
https://github.com/bitwarden/browser
synced 2024-12-25 01:22:26 +01:00
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:
parent
3a34d3b174
commit
04014a8e78
141
spec/common/misc/sequentialize.spec.ts
Normal file
141
spec/common/misc/sequentialize.spec.ts
Normal 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
52
src/misc/sequentialize.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user