mojito
Version:
Mojito provides an architecture, components and tools for developers to build complex web applications faster.
390 lines (343 loc) • 12.5 kB
JavaScript
/*
* Copyright (c) 2011-2013, Yahoo! Inc. All rights reserved.
* Copyrights licensed under the New BSD License.
* See the accompanying LICENSE file for terms.
*/
/*jslint anon:true, node: true, nomen:true*/
/*global YUI,require*/
YUI.add('mojito-perf', function (Y, NAME) {
'use strict';
/**
* @module mojito-perf
* @class MojitoPerf
* @static
*/
var libfs = require('fs'),
buffer = {},
config = Y.config.perf,
requestId = 0,
colorRed = '\u001b[31m',
colorReset = '\u001b[0m',
getgo,
microtime;
try {
microtime = require('microtime');
} catch (e) {
Y.log('microtime not found. Recorded times will not have' +
' microsecond accuracy', 'warn', NAME);
}
//internal. write perf info into a file
function writeLog(filename, logs) {
var outstream,
i;
try {
outstream = libfs.createWriteStream(filename.replace('{req}', requestId), {
flags: 'a' // append
});
for (i = 0; i < logs.length; i += 1) {
outstream.write(logs[i].join('|') + "\n");
}
outstream.end();
outstream = null;
} catch (err) {
Y.log('Error trying to dump perf metrics in file: ' +
filename + ' Error:' + err, 'error', NAME);
}
}
//internal. print perf info in the logs
function print(group, key) {
var o = buffer[group][key],
type = (o.ms ? 'TIMELINE' : 'MARK'),
// if we already have milliseconds, good
// if not, we can compute it based on request init
time = o.time,
offset = o.time - getgo,
duration = o.ms || '',
desc = o.msg || 'no description',
label = o.label,
id = o.id;
if ((config.mark && !o.ms) || (config.timeline && o.ms)) {
Y.log(group + ':' + key + ' ' + type + colorReset +
' offset=' + colorRed + offset + colorReset +
(o.ms ? ' duration=' + colorRed + duration + colorReset : '') +
' (' + desc + ')',
'mojito', NAME);
return [type, requestId, time, duration, group, label, id, desc];
}
}
//internal. abstracts where timestamps come from
function timestamp() {
return microtime ? microtime.now() : new Date().getTime();
}
/**
* Produces an ID to identify the timeline or mark based on a
* command object.
*
* @method idFromCommand
* @private
* @param {object} command Object that represent the command to invoke.
* @return {string} ID that represents the command.
**/
function idFromCommand(command) {
var str;
if (command && command.instance) {
if (command.instance.id) {
str = command.instance.id;
} else if (command.instance.base) {
str = '+' + command.instance.base;
} else {
str = '@' + command.instance.type;
}
str += '.' + (command.action || command.instance.action || '???');
}
return str;
}
/**
* Sets a mark in the request timeline. All marks will be flushed
* after the end. This is useful to measure when a particular process
* start or end with respect to the request timeline.
*
* @method mark
* @param {string} group Event group.
* @param {string} label Event identifier. Will be combined with group.
* @param {string} msg Description of the mark.
* @param {string|object} id Unique identifier of the mark, usually
* the requestId or a command object.
* @return {Object} The mark entry.
**/
function mark(group, label, msg, id) {
var s,
key = label;
if (!group || !label) {
return;
}
if (id) {
// we might also accept a command object
id = Y.Lang.isObject(id) ? idFromCommand(id) : id;
key += '[' + id + ']';
}
if (!buffer[group]) {
buffer[group] = {};
}
if (!msg) {
msg = '';
}
if (buffer[group][key]) {
Y.log('Perf metric collision for group=' + group +
' label=' + label + ' id=' + id +
'. Measure one thing at a time.', 'warn', NAME);
key += Y.guid();
}
s = buffer[group][key] = {};
s.msg = msg;
s.label = label;
s.id = id;
s.time = timestamp();
return s;
}
/**
* Starts a timeline metric, providing a way to call it done
* at some point in the future. This is useful to measure the
* time to execute a process in mojito.
*
* @method timeline
* @param {string} group Event group.
* @param {string} label Event identifier. Will be combined with group.
* @param {string} msg Description of the mark.
* @param {string} id Unique identifier of the mark, usually
* the requestId or the yuid().
* @return {object} represents the timeline object that has a method
* called "done" that can be invoked when the process finish.
**/
function timeline(group, label, msg, id) {
var m = mark(group, label, msg, id);
return {
done: function () {
m.ms = timestamp() - m.time;
}
};
}
/**
* Dumps all marks and timeline entries into the console.
* This method is meant to be called automatically when
* a request ends. You can target specific metrics by using
* the configuration:
*
* "perf": {
* "include": {
* "mojito-action-context": true
* }
* }
*
* Or just exclude some of them by doing:
*
* "perf": {
* "exclude": {
* "mojito-action-context": true
* }
* }
*
*
* @method dump
* @private
* @return {array} collection of perf logs. Each item will expose:
* {type, requestId, time, duration, group, label, id, desc}
**/
function dump() {
var group,
key,
entry,
logs = [];
for (group in buffer) {
if ((buffer.hasOwnProperty(group)) &&
(!config.exclude || !config.exclude[group]) &&
(!config.include || config.include[group])) {
for (key in buffer[group]) {
if (buffer[group].hasOwnProperty(key)) {
entry = print(group, key);
if (entry) {
logs.push(entry);
}
}
}
}
}
buffer = {};
// dumping to disk
if (config.logFile) {
Y.log('Dumping performance metrics into disk: ' +
config.logFile, 'mojito', NAME);
writeLog(config.logFile, logs);
}
return logs;
}
/**
* Instruments requests that will be processed by mojito
* core, providing a valid timeline for that request, and
* allowing to instrument some other relative processes,
* and grouping them per request to facilitate analysis.
* This method is responsible for calling "dump".
*
* @method instrumentMojitoRequest
* @param {object} req the request object from express.
* @param {object} res the response object from express.
**/
function instrumentMojitoRequest(req, res) {
var id = (requestId += 1),
perf,
end = res.end;
getgo = timestamp();
if (Y.Object.keys(buffer).length > 0) {
Y.log('Multiple requests at the same time. This can ' +
'mess with the perf analysis. Curl is your best ' +
'friend, use it.', 'warn', NAME);
}
perf = timeline('mojito', 'request', 'the whole request', id);
// hooking into the res.end called from output-handler.server.js
// to be able to flush perf metrics only for mojito requests.
// static requests and other type of requests will be ignored.
res.end = function () {
if (perf) {
end.apply(res, arguments);
Y.log('Flushing perf metrics', 'mojito', NAME);
perf.done();
dump();
// some cleanup
perf = null;
end = null;
req = null;
res = null;
}
};
}
if (config) {
Y.namespace('mojito.perf');
Y.mojito.perf.idFromCommand = idFromCommand;
Y.mojito.perf.instrumentMojitoRequest = instrumentMojitoRequest;
Y.mojito.perf.dump = dump;
// overriding the default definitions if needed
if (config.timeline) {
Y.mojito.perf.timeline = timeline;
}
if (config.mark) {
Y.mojito.perf.mark = mark;
}
} else {
config = {};
}
// Hook profiles in
Y.mojito.hooks.registerHook(NAME, 'adapterBuffer', function(w, adapter) {
if (w === 'start') {
this.ab_perf = Y.mojito.perf.timeline('mojito-composite-addon', 'child', 'the whole child', adapter.id);
} else {
this.ab_perf.done();
}
});
Y.mojito.hooks.registerHook(NAME, 'addon', function(w, addOn, cfg) {
if (w === 'start') {
this.ad_perf = Y.mojito.perf.timeline('mojito-composite-addon', 'execute', Y.Object.keys(cfg.children).join(','), addOn.ac.command);
} else {
this.ad_perf.done();
}
});
Y.mojito.hooks.registerHook(NAME, 'hb', function(w, tmpl) {
if (w === 'start') {
this.hb_perf = Y.mojito.perf.timeline('mojito', 'hb:render', 'time to render a template', tmpl);
} else {
this.hb_perf.done();
}
});
Y.mojito.hooks.registerHook(NAME, 'attachActionContext', function(w, command) {
if (w === 'start') {
this.acc_perf = Y.mojito.perf.timeline('mojito', 'ac:addons', 'attaching addons to AC object', command);
} else {
this.acc_perf.done();
}
});
Y.mojito.hooks.registerHook(NAME, 'actionContext', function(w, ac, opts) {
if (w === 'start') {
this.ac_perf = Y.mojito.perf.timeline('mojito', 'ac:init', 'set up AC object', opts.command);
} else if (w === 'end1') {
this.ac_perf.done();
Y.mojito.perf.mark('mojito', 'action:start', 'before the action', opts.command);
this.ac_perf = Y.mojito.perf.timeline('mojito', 'action:call', 'the initial syncronous part of the action', opts.command);
} else {
this.ac_perf.done();
}
});
Y.mojito.hooks.registerHook(NAME, 'actionContextDone', function(w, ac) {
if (w === 'start') {
this.acd_perf = Y.mojito.perf.timeline('mojito', 'ac.done', 'time to execute ac.done process', ac.command);
} else if (w === 'end1') {
this.acd_perf.done();
} else {
this.acd_perf.done();
Y.mojito.perf.mark('mojito', 'action:stop', 'after the action', ac.command);
}
});
Y.mojito.hooks.registerHook(NAME, 'dispatchCreateAction', function(w, command) {
if (w === 'start') {
this.dac_perf = Y.mojito.perf.timeline('mojito', 'ac:ctor', 'create ControllerContext', command);
} else {
this.dac_perf.done();
}
});
Y.mojito.hooks.registerHook(NAME, 'dispatch', function(w, command) {
if (w === 'start') {
this.dis_perf = Y.mojito.perf.timeline('mojito', 'dispatch:expandInstance', 'gather details about mojit', command);
} else {
this.dis_perf.done();
}
});
Y.mojito.hooks.registerHook(NAME, 'AppDispatch', function(req, res) {
// if perf metrics are on, we should hook into
// the mojito request to flush metrics when
// the connection is closed.
if (Y.mojito.perf.instrumentMojitoRequest) {
Y.mojito.perf.instrumentMojitoRequest(req, res);
}
});
}, '0.1.0', {requires: [
'mojito',
'mojito-hooks'
]});