@webda/profiler
Version:
Webda Basic Profiler
187 lines • 5.99 kB
JavaScript
import { LoggerService, Service, ServiceParameters } from "@webda/core";
/**
* Profiler Parameters
*/
export class ProfilerParameters extends ServiceParameters {
/**
* @inheritdoc
*/
constructor(params) {
super(params);
this.disabled ?? (this.disabled = false);
}
}
/**
* Profiling service
*
* Mesure timing of each method and display them in TRACE
*
* @WebdaModda
*/
export default class Profiler extends Service {
/**
* @inheritdoc
*/
loadParameters(params) {
return new ProfilerParameters(params);
}
/**
* Return all methods from an object
*
* @param obj to return methods from
*/
getMethods(obj) {
let properties = new Set();
let currentObj = Object.getPrototypeOf(obj);
do {
Object.getOwnPropertyNames(currentObj)
.filter(i => ["constructor", "on", "once"].indexOf(i) < 0)
.forEach(item => properties.add(item));
} while ((currentObj = Object.getPrototypeOf(currentObj)));
return [...properties.keys()].filter((item) => typeof obj[item] === "function");
}
/**
* Method to call before original function
* @param {Service} service being patched
* @param method method being patch
*/
preprocessor(_service, _method) {
return {
start: Date.now()
};
}
/**
* Method to call after original function
* @param service being patched
* @param method method being patch
* @param data return by the preprocessor
* @param err error thrown by the method if any
*/
postprocessor(service, method, data, err) {
let duration = Date.now() - data.start;
if (err) {
this.logMetrics(`${service.getName()}.${method}: ${duration}ms - ERROR ${err}`);
}
else {
this.logMetrics(`${service.getName()}.${method}: ${duration}ms`);
}
}
/**
* Log the performance
* @param args
*/
logMetrics(...args) {
this.log("TRACE", ...args);
}
/**
* Return true if the service should not be instrumentalized
* Logger service are excluded
*
* @param service to check
*/
excludeService(service) {
return service instanceof LoggerService || service instanceof Profiler || this === service;
}
/**
* Patch all services method: interlacing our pre/post processor
*/
patchServices(services) {
for (let i in services) {
// Check if service is excluded
if (this.excludeService(services[i])) {
continue;
}
this.log("TRACE", `Profiling patching ${services[i]._name}`);
let methods = this.getMethods(services[i]);
for (let mi in methods) {
let m = methods[mi];
// Skip getName as we use it
if (["getName"].includes(m)) {
continue;
}
((service, method) => {
const originalMethod = services[service][method];
services[service][method] = (...args) => {
if (!this.isEnabled()) {
return originalMethod.bind(services[service], ...args)();
}
let data = this.preprocessor(services[service], method);
let res;
try {
res = originalMethod.bind(services[service], ...args)();
}
catch (err) {
this.postprocessor(services[service], method, data, err);
throw err;
}
if (res instanceof Promise) {
return res
.then(r => {
this.postprocessor(services[service], method, data);
return r;
})
.catch(r => {
this.postprocessor(services[service], method, data, r.message);
throw r;
});
}
this.postprocessor(services[service], method, data);
return res;
};
})(i, m);
}
}
}
/**
* Return if Profiler is enable
*/
isEnabled() {
return !this.parameters.disabled;
}
/**
* Instrument Request to display request duration in ms
*
* @param {Context} request to instrument
* @param {any[]}
*/
instrumentRequest(ctx, ..._args) {
const exec = ctx.execute.bind(ctx);
// Dynamic replace the execute functionc
ctx.execute = async () => {
let start = Date.now();
let error;
try {
await exec();
}
catch (err) {
error = err;
throw err;
}
finally {
if (error) {
this.logMetrics(`Request took ${Date.now() - start}ms - ERROR ${error.message}`);
}
else {
this.logMetrics(`Request took ${Date.now() - start}ms`);
}
}
};
}
/**
* Add listeners on `Webda.Init.Services` and `Webda.Request`
*/
resolve() {
this._webda.on("Webda.Init.Services", async (services) => {
this.patchServices(services);
});
this._webda.on("Webda.Request", evt => {
if (!this.isEnabled()) {
return;
}
this.instrumentRequest(evt.context);
});
return this;
}
}
export { Profiler };
//# sourceMappingURL=profiler.js.map