Skip to content

Instantly share code, notes, and snippets.

@iamlothian
Last active October 2, 2019 07:11
Show Gist options
  • Save iamlothian/4e7229ddefc86d6e5565f39c26a3480b to your computer and use it in GitHub Desktop.
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
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;
};
}
@iamlothian
Copy link
Author

iamlothian commented Oct 2, 2019

A quick example of usage

import { profile, profileify, profileifyFn } from './Utils/profilingUtils';

function profileMetric() {
    return profile(
        (target: any, propertyName: keyof any) => {
            const message = `begin ${target.constructor.name}.${propertyName.toString()}`;
            monitoring.trackTrace({message});
            // console.info(message);
        },
        (target: object, propertyName: keyof any, args: any[], result: any, didError: boolean, duration: number, receiver: any) => {
            monitoring.trackMetric({name: `${target.constructor.name}.${propertyName.toString()}.duration (ms)`, value: duration});
            const message = `end ${target.constructor.name}.${propertyName.toString()} - duration: ${duration}ms ${didError ? '[with Error]' : ''}`;
            // console.log(message);
            monitoring.trackTrace({message});
        },
        process.env.PROFILE_METRICS === 'false' || true
    );
}

// profile 3rd party instance method access
const repository = profileify(someORM.getRepository(/*...*/))

export default class FileStorageService {

    // profile your own class methods
    @profileMetric()
    public async uploadStream(file: FileInput): Promise<Media> {

       // profile a function
       const proxyUploadStreamToBlockBlob = profileifyFn(uploadStreamToBlockBlob, () => ``, trackDependency('serviceDomain', 'Media', 'BlobStorage'));
       const uploadBlobResponse = await proxyUploadStreamToBlockBlob(/* args */);

       repository.save(/* Entity */);
    }

}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment