227 lines
6.3 KiB
JavaScript
227 lines
6.3 KiB
JavaScript
|
/*
|
|||
|
* SystemJS performance measurement addon
|
|||
|
*
|
|||
|
*/
|
|||
|
(function() {
|
|||
|
function perfEvent(eventName, eventItem) {
|
|||
|
var evt = new ProfileEvent(eventName, eventItem);
|
|||
|
events.push(evt);
|
|||
|
return evt;
|
|||
|
};
|
|||
|
|
|||
|
// we extend the hook function itself to create performance instrumentation per extension
|
|||
|
var curHook = hook;
|
|||
|
|
|||
|
var resolverHooks = ['import', 'normalizeSync', 'decanonicalize', 'normalize'];
|
|||
|
var pipelineHooks = ['locate', 'fetch', 'translate', 'instantiate'];
|
|||
|
hook = function(name, newHook) {
|
|||
|
// hook information
|
|||
|
var resolver = resolverHooks.indexOf(name) != -1;
|
|||
|
var pipeline = pipelineHooks.indexOf(name) != -1;
|
|||
|
var hookEvtName = name + ':' + newHook.toString().substr(0, 200);
|
|||
|
|
|||
|
curHook(name, function(prevHook) {
|
|||
|
return function() {
|
|||
|
// this is called when the hook runs
|
|||
|
var hookItem = resolver ? arguments[0] + ' : ' + arguments[1] + ' : ' + !!arguments[2] : arguments[0] && arguments[0].name;
|
|||
|
var evt = perfEvent(hookEvtName, hookItem);
|
|||
|
|
|||
|
// ignore time spent in the old hook
|
|||
|
var fn = newHook(function() {
|
|||
|
evt.pause();
|
|||
|
var output = prevHook.apply(this, arguments);
|
|||
|
|
|||
|
if (output && output.toString && output.toString() == '[object Promise]') {
|
|||
|
return Promise.resolve(output)
|
|||
|
.then(function(output) {
|
|||
|
evt.resume();
|
|||
|
return output;
|
|||
|
})
|
|||
|
.catch(function(err) {
|
|||
|
evt.resume()
|
|||
|
throw err;
|
|||
|
});
|
|||
|
}
|
|||
|
else {
|
|||
|
evt.resume();
|
|||
|
return output;
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
var output;
|
|||
|
try {
|
|||
|
output = fn.apply(this, arguments);
|
|||
|
}
|
|||
|
catch(e) {
|
|||
|
evt.cancel();
|
|||
|
throw e;
|
|||
|
}
|
|||
|
|
|||
|
if (output && output.toString && output.toString() == '[object Promise]') {
|
|||
|
return Promise.resolve(output)
|
|||
|
.then(function(output) {
|
|||
|
evt.done();
|
|||
|
return output;
|
|||
|
})
|
|||
|
.catch(function(err) {
|
|||
|
evt.cancel();
|
|||
|
throw err;
|
|||
|
});
|
|||
|
}
|
|||
|
else {
|
|||
|
evt.done();
|
|||
|
return output;
|
|||
|
}
|
|||
|
};
|
|||
|
});
|
|||
|
};
|
|||
|
|
|||
|
// profiling events
|
|||
|
var events = [];
|
|||
|
|
|||
|
var perf = typeof performance != 'undefined' : performance : Date;
|
|||
|
|
|||
|
// Performance Event class
|
|||
|
function ProfileEvent(name, item) {
|
|||
|
this.name = name;
|
|||
|
this.item = (typeof item == 'function' ? item() : item) || 'default';
|
|||
|
this.start = perf.now();
|
|||
|
this.stop = null;
|
|||
|
this.pauseTime = null;
|
|||
|
this.cancelled = false;
|
|||
|
}
|
|||
|
ProfileEvent.prototype.rename = function(name, item) {
|
|||
|
this.name = name;
|
|||
|
if (arguments.length > 1)
|
|||
|
this.item = item;
|
|||
|
};
|
|||
|
ProfileEvent.prototype.done = function() {
|
|||
|
if (this.stop)
|
|||
|
throw new TypeError('Event ' + this.name + ' (' + this.item + ') has already completed.');
|
|||
|
this.stop = perf.now();
|
|||
|
};
|
|||
|
ProfileEvent.prototype.cancel = function() {
|
|||
|
this.cancelled = true;
|
|||
|
};
|
|||
|
ProfileEvent.prototype.cancelIfNotDone = function() {
|
|||
|
if (!this.stop)
|
|||
|
this.cancelled = true;
|
|||
|
};
|
|||
|
ProfileEvent.prototype.pause = function() {
|
|||
|
if (this.stop)
|
|||
|
throw new TypeError('Event ' + this.name + ' (' + this.item + ') cannot be paused as it has finished.');
|
|||
|
if (!this.pauseTime)
|
|||
|
this.pauseTime = perf.now();
|
|||
|
};
|
|||
|
ProfileEvent.prototype.resume = function() {
|
|||
|
if (!this.pauseTime)
|
|||
|
throw new TypeError('Event ' + this.name + ' (' + this.item + ') is not already paused.');
|
|||
|
this.start += perf.now() - this.pauseTime;
|
|||
|
this.pauseTime = null;
|
|||
|
};
|
|||
|
|
|||
|
var logged = false;
|
|||
|
SystemJSLoader.prototype.perfSummary = function(includeEvts) {
|
|||
|
logged = true;
|
|||
|
// create groupings of events by event name to time data
|
|||
|
// filtering out cancelled events
|
|||
|
var groupedEvents = {};
|
|||
|
events.forEach(function(evt) {
|
|||
|
if (includeEvts && !includeEvts(evt))
|
|||
|
return;
|
|||
|
if (evt.cancelled)
|
|||
|
return;
|
|||
|
if (!evt.stop) {
|
|||
|
console.warn('Event ' + evt.name + ' (' + evt.item + ') never completed.');
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
var evtTimes = groupedEvents[evt.name] = groupedEvents[evt.name] || [];
|
|||
|
evtTimes.push({
|
|||
|
time: evt.stop - evt.start,
|
|||
|
item: evt.item
|
|||
|
});
|
|||
|
});
|
|||
|
|
|||
|
Object.keys(groupedEvents).forEach(function(evt) {
|
|||
|
var evtTimes = groupedEvents[evt];
|
|||
|
|
|||
|
// only one event -> log as a single event
|
|||
|
if (evtTimes.length == 1) {
|
|||
|
console.log(toTitleCase(evt) + (evtTimes[0].item != 'default' ? ' (' + evtTimes[0].item + ')' : ''));
|
|||
|
logStat('Total Time', evtTimes[0].time);
|
|||
|
console.log('');
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
// multiple events, give some stats!
|
|||
|
var evtCount = evtTimes.length;
|
|||
|
|
|||
|
console.log(toTitleCase(evt) + ' (' + evtCount + ' events)');
|
|||
|
|
|||
|
var totalTime = evtTimes.reduce(function(curSum, evt) {
|
|||
|
return curSum + evt.time;
|
|||
|
}, 0);
|
|||
|
logStat('Cumulative Time', totalTime);
|
|||
|
|
|||
|
var mean = totalTime / evtCount;
|
|||
|
logStat('Mean', mean);
|
|||
|
|
|||
|
var stdDev = Math.sqrt(evtTimes.reduce(function(curSum, evt) {
|
|||
|
return curSum + Math.pow(evt.time - mean, 2);
|
|||
|
}, 0) / evtCount);
|
|||
|
|
|||
|
logStat('Std Deviation', stdDev);
|
|||
|
|
|||
|
var withoutOutliers = evtTimes.filter(function(evt) {
|
|||
|
return evt.time > mean - stdDev && evt.time < mean + stdDev;
|
|||
|
});
|
|||
|
|
|||
|
logStat('Avg within 2σ', withoutOutliers.reduce(function(curSum, evt) {
|
|||
|
return curSum + evt.time;
|
|||
|
}, 0) / withoutOutliers.length);
|
|||
|
|
|||
|
var sorted = evtTimes.sort(function(a, b) {
|
|||
|
return a.time > b.time ? 1 : -1;
|
|||
|
});
|
|||
|
|
|||
|
var medianIndex = Math.round(evtCount / 2);
|
|||
|
logStat('Median', sorted[medianIndex].time, sorted[medianIndex].evt);
|
|||
|
|
|||
|
logStat('Max', sorted[evtCount - 1].time, sorted[evtCount - 1].item);
|
|||
|
|
|||
|
logStat('Min', sorted[0].time, sorted[0].item);
|
|||
|
|
|||
|
var duplicates = evtTimes.filter(function(evt) {
|
|||
|
return evtTimes.some(function(dup) {
|
|||
|
return dup !== evt && dup.name == evt.name && dup.item == evt.item;
|
|||
|
});
|
|||
|
});
|
|||
|
|
|||
|
logStat('Duplicate Events', duplicates.length, true);
|
|||
|
|
|||
|
logStat('Total Duplicated Time', duplicates.reduce(function(duplicateTime, evt) {
|
|||
|
return duplicateTime + evt.time;
|
|||
|
}, 0));
|
|||
|
|
|||
|
console.log('');
|
|||
|
});
|
|||
|
};
|
|||
|
|
|||
|
function toTitleCase(title) {
|
|||
|
return title.split('-').map(function(part) {
|
|||
|
return part[0].toUpperCase() + part.substr(1);
|
|||
|
}).join(' ');
|
|||
|
}
|
|||
|
|
|||
|
var titleLen = 25;
|
|||
|
function logStat(title, value, item, isNum) {
|
|||
|
if (item === true) {
|
|||
|
item = undefined;
|
|||
|
isNum = true;
|
|||
|
}
|
|||
|
var spaces = Array(titleLen - title.length + 1).join(' ');
|
|||
|
var value = isNum ? value : Math.round(value * 100) / 100 + 'ms';
|
|||
|
console.log(' ' + title + spaces + ': ' + value + (item ? ' (' + item + ')' : ''));
|
|||
|
}
|
|||
|
})();
|