Last active
October 2, 2019 07:11
-
-
Save iamlothian/4e7229ddefc86d6e5565f39c26a3480b to your computer and use it in GitHub Desktop.
wrap any object, function or class method in a proxy that will expose pre and post handlers with a built in timer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export type preFunc<T> = (target: T, propertyName: keyof T, args: any[], receiver: any) => void; | |
export type postFunc<T> = (target: T, propertyName: keyof T, args: any[], result: any, didError: boolean, duration: number, receiver: any) => void; | |
function postDefault<T extends object>(target: T, propertyName: keyof T, args: any[], result: any, didError: boolean, duration: number, receiver: any) { | |
console.log(`${target.constructor.name}.${propertyName.toString()} - duration: ${duration}ms ${didError ? '[with Error]' : ''}`); | |
} | |
/** | |
* wrap any object in a proxy that will expose pre and post handlers with a built in timer | |
* @param source the object to proxy | |
* @param preCb called before method | |
* @param postCb called after method | |
* @param methodBlacklist never proxy methods in this list | |
* @param enabled disable wrapping and always return source | |
*/ | |
export function profileify<T extends object>( | |
source: T, | |
preCb: preFunc<T> = () => ``, | |
postCb: postFunc<T> = postDefault, | |
methodBlacklist: string[] = [], | |
enabled: boolean = true | |
): T { | |
const handler = { | |
get(target: T, prop: keyof T, receiver: any) { | |
// remember original method | |
const original = Reflect.get(target, prop, receiver); | |
if (!enabled) { | |
return original; | |
} | |
// only profile function method that are OwnProperty of target and not in blacklist | |
const canProxy = | |
Object.getPrototypeOf(target).hasOwnProperty(prop) | |
&& typeof original === 'function' | |
&& !methodBlacklist.includes(prop as string); | |
// return proxy or original value | |
const method = !canProxy | |
? original | |
// async in case we wrap a promise | |
: ( ... args: any[]) => { | |
let error = true; | |
// tslint:disable-next-line: no-unnecessary-initializer | |
let result: T = source; | |
const start = Date.now(); | |
preCb(target, prop, args, receiver); | |
try { | |
result = original.apply(receiver, args); | |
error = false; | |
return result; | |
} finally { | |
// always call postCb // await result if promise | |
result instanceof Promise | |
// tslint:disable-next-line: no-unused-expression | |
? result.finally(() => postCb(target, prop, args, result!, error, Date.now() - start, receiver)) | |
: postCb(target, prop, args, result!, error, Date.now() - start, receiver); | |
} | |
}; | |
return method; | |
} | |
}; | |
return new Proxy(source, handler); | |
} | |
/** | |
* wrap an isolated function in a profiler | |
* @param source the function | |
* @param preCb called before method | |
* @param postCb called after method | |
* @param enabled disable wrapping and always return source | |
*/ | |
// tslint:disable-next-line: ban-types | |
export function profileifyFn<F extends Function>( | |
source: F, | |
preCb: preFunc<any> = () => ``, | |
postCb: postFunc<any> = postDefault, | |
enabled: boolean = true, | |
alias?: string | |
): F { | |
const canProxy: boolean = enabled && typeof source === 'function'; | |
const proxy = function(this: any, ... args: any[]) { | |
let error = true; | |
let result: any; | |
const start = Date.now(); | |
preCb(source, alias || source.name, args, this); | |
try { | |
result = source.apply(this, args); | |
error = false; | |
return result; | |
} finally { | |
result instanceof Promise // await result if promise | |
// tslint:disable-next-line: no-unused-expression | |
? result.finally(() => postCb(source, alias || source.name, args, result!, error, Date.now() - start, this)) | |
: postCb(source, alias || source.name, args, result!, error, Date.now() - start, this); | |
} | |
}; | |
return canProxy | |
// tslint:disable-next-line: ban-types | |
? (proxy as Function) as F | |
: source; | |
} | |
/** | |
* Annotation/decorator for profiling class methods by wrapping in a proxy function | |
* @param preCb called before method | |
* @param postCb called after method | |
* @param enabled disable wrapping and always return source | |
*/ | |
export function profile( | |
preCb: preFunc<any> = (target: any, propertyName: keyof any) => console.info(`begin ${target.constructor.name}.${propertyName.toString()}`), | |
postCb: postFunc<any> = (target: any, propertyName: keyof any, args: any[], result: any, didError: boolean, duration: number, receiver: any) => | |
console.log(`end ${target.constructor.name}.${propertyName.toString()} - duration: ${duration}ms ${didError ? '[with Error]' : ''}`), | |
enabled: boolean = true | |
) { | |
return (target: any, key: string | symbol, descriptor: PropertyDescriptor) => { | |
if (!enabled) { | |
return descriptor; | |
} | |
const original = descriptor.value; | |
descriptor.value = function( ... args: any[]) { | |
let error = true; | |
let result: any; | |
const start = Date.now(); | |
preCb(target, key, args, this); | |
try { | |
result = original.apply(this, args); | |
error = false; | |
return result; | |
} finally { | |
result instanceof Promise // await result if promise | |
// tslint:disable-next-line: no-unused-expression | |
? result.finally(() => postCb(target, key, args, result!, error, Date.now() - start, this)) | |
: postCb(target, key, args, result!, error, Date.now() - start, this); | |
} | |
}; | |
return descriptor; | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
A quick example of usage