Skip to content

Commit 7c3fede

Browse files
committed
feat(): lru memoization for js worker
1 parent 1de0465 commit 7c3fede

4 files changed

Lines changed: 99 additions & 9 deletions

File tree

lib/memoize/index.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import LRUCache from 'lru-cache';
2+
import Crypto from '../utils/crypto';
3+
4+
/**
5+
* Pick the strategy of cache key form
6+
* It could be concatenated list of arguments like 'projectId:eventId'
7+
* Or it could be hashed json object — blake2b512 algorithn
8+
*/
9+
export type MemoizeKeyStrategy = 'concat' | 'hash';
10+
11+
/**
12+
* Options of the memoize decorator
13+
*/
14+
export interface MemoizeOptions {
15+
/**
16+
* Max number of values stored in LRU cache at the same time
17+
*/
18+
max?: number;
19+
20+
/**
21+
* TTL in milliseconds
22+
*/
23+
ttl?: number;
24+
25+
/**
26+
* Strategy for key generation
27+
*/
28+
strategy?: MemoizeKeyStrategy;
29+
}
30+
31+
/**
32+
* Async-only, per-method LRU-backed memoization decorator.
33+
* Cache persists for the lifetime of the class instance (e.g. worker).
34+
*/
35+
export function Memoize(options: MemoizeOptions = {}): MethodDecorator {
36+
const {
37+
max = 50,
38+
ttl = 1000 * 60 * 30,
39+
strategy = 'concat',
40+
} = options;
41+
42+
return function (
43+
_target,
44+
propertyKey,
45+
descriptor: PropertyDescriptor
46+
): PropertyDescriptor {
47+
const originalMethod = descriptor.value;
48+
49+
if (typeof originalMethod !== 'function') {
50+
throw new Error('@Memoize can only decorate methods');
51+
}
52+
53+
descriptor.value = async function (...args: unknown[]): Promise<unknown> {
54+
/**
55+
* Create a cache key for each decorated method
56+
*/
57+
const cacheKey = `memoizeCache:${String(propertyKey)}`;
58+
59+
/**
60+
* Create a new cache if it does not exists yet (for certain function)
61+
*/
62+
const cache: LRUCache<string, any> = this[cacheKey] ??= new LRUCache<string, any>({ max, ttl });
63+
64+
const key = strategy === 'hash'
65+
? Crypto.hash(args)
66+
: args.map(String).join(':');
67+
68+
if (cache.has(key)) {
69+
return cache.get(key);
70+
}
71+
72+
try {
73+
const result = await originalMethod.apply(this, args);
74+
cache.set(key, result);
75+
return result;
76+
} catch (err) {
77+
cache.delete(key);
78+
throw err;
79+
}
80+
};
81+
82+
return descriptor;
83+
};
84+
}

lib/utils/crypto.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import crypto from 'crypto';
1+
import crypto, { BinaryToTextEncoding } from 'crypto';
22

33
/**
44
* Crypto helper
@@ -10,11 +10,11 @@ export default class Crypto {
1010
* @param value — data to be hashed
1111
* @param algo — type of algorithm to be used for hashing
1212
*/
13-
public static hash(value: unknown, algo = 'sha256'): string {
13+
public static hash(value: unknown, algo = 'sha256', digest: BinaryToTextEncoding = 'hex'): string {
1414
const stringifiedValue = JSON.stringify(value);
1515

1616
return crypto.createHash(algo)
1717
.update(stringifiedValue)
18-
.digest('hex');
18+
.digest(digest);
1919
}
2020
}

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
5959

6060
/* Experimental Options */
61-
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
61+
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
6262
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
6363

6464
/* Advanced Options */

workers/javascript/src/index.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import { beautifyUserAgent } from './utils';
1414
import { Collection } from 'mongodb';
1515
import { parse } from '@babel/parser';
1616
import traverse from '@babel/traverse';
17+
import { Memoize } from '../../../lib/memoize';
18+
19+
const MEMOIZATION_TTL = Number(process.env.MEMOIZATION_TTL ?? 0);
1720

1821
/**
1922
* Worker for handling Javascript events
@@ -234,9 +237,9 @@ export default class JavascriptEventWorker extends EventWorker {
234237
*/
235238
lines = this.readSourceLines(consumer, originalLocation);
236239

237-
// const originalContent = consumer.sourceContentFor(originalLocation.source);
240+
const originalContent = consumer.sourceContentFor(originalLocation.source);
238241

239-
// functionContext = this.getFunctionContext(originalContent, originalLocation.line) ?? originalLocation.name;
242+
functionContext = this.getFunctionContext(originalContent, originalLocation.line) ?? originalLocation.name;
240243
} catch(e) {
241244
HawkCatcher.send(e);
242245
this.logger.error('Can\'t get function context');
@@ -260,7 +263,8 @@ export default class JavascriptEventWorker extends EventWorker {
260263
* @param line - number of the line from the stack trace
261264
* @returns {string | null} - string of the function context or null if it could not be parsed
262265
*/
263-
private _getFunctionContext(sourceCode: string, line: number): string | null {
266+
@Memoize({ max: 50, ttl: MEMOIZATION_TTL, strategy: 'hash' })
267+
private getFunctionContext(sourceCode: string, line: number): string | null {
264268
let functionName: string | null = null;
265269
let className: string | null = null;
266270
let isAsync = false;
@@ -363,11 +367,12 @@ export default class JavascriptEventWorker extends EventWorker {
363367
*
364368
* @param map - saved file info without content.
365369
*/
366-
private loadSourceMapFile(map: SourceMapDataExtended): Promise<string> {
370+
@Memoize({ max: 50, ttl: MEMOIZATION_TTL })
371+
private loadSourceMapFile(mapId: SourceMapDataExtended['_id']): Promise<string> {
367372
return new Promise((resolve, reject) => {
368373
let buf = Buffer.from('');
369374

370-
const readstream = this.db.getBucket().openDownloadStream(map._id)
375+
const readstream = this.db.getBucket().openDownloadStream(mapId)
371376
.on('data', (chunk) => {
372377
buf = Buffer.concat([buf, chunk]);
373378
})
@@ -450,6 +455,7 @@ export default class JavascriptEventWorker extends EventWorker {
450455
*
451456
* @param {string} mapBody - source map content
452457
*/
458+
@Memoize({ max: 50, ttl: MEMOIZATION_TTL, strategy: 'hash' })
453459
private consumeSourceMap(mapBody: string): SourceMapConsumer {
454460
try {
455461
const rawSourceMap = JSON.parse(mapBody);

0 commit comments

Comments
 (0)