diff options
Diffstat (limited to 'lib/perf.js')
-rw-r--r-- | lib/perf.js | 227 |
1 files changed, 227 insertions, 0 deletions
diff --git a/lib/perf.js b/lib/perf.js new file mode 100644 index 000000000..9f0a02fbc --- /dev/null +++ b/lib/perf.js @@ -0,0 +1,227 @@ +/* + * 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 + ')' : '')); +} +})();
\ No newline at end of file |