lib/models/instrumentation.js
'use strict';
const fs = require('fs-extra');
const chalk = require('chalk');
const heimdallGraph = require('heimdalljs-graph');
const getConfig = require('../utilities/get-config');
const utilsInstrumentation = require('../utilities/instrumentation');
const logger = require('heimdalljs-logger')('ember-cli:instrumentation');
const hwinfo = require('./hardware-info');
let vizEnabled = utilsInstrumentation.vizEnabled;
let instrumentationEnabled = utilsInstrumentation.instrumentationEnabled;
function _enableFSMonitorIfInstrumentationEnabled(config) {
let monitor;
if (instrumentationEnabled(config)) {
const FSMonitor = require('heimdalljs-fs-monitor');
monitor = new FSMonitor();
monitor.start();
}
return monitor;
}
_enableFSMonitorIfInstrumentationEnabled();
function _getHardwareInfo(platform) {
const startTime = process.hrtime();
Object.getOwnPropertyNames(hwinfo).forEach((metric) => (platform[metric] = hwinfo[metric]()));
const collectionTime = process.hrtime(startTime);
// Convert from integer [seconds, nanoseconds] to floating-point milliseconds.
platform.collectionTime = collectionTime[0] * 10e3 + collectionTime[1] / 10e6;
}
class Instrumentation {
/**
An instance of this class is used for invoking the instrumentation
hooks on addons.
The instrumentation types currently supported are:
* init
* build
* command
* shutdown
@class Instrumentation
@private
*/
constructor(options) {
this.isVizEnabled = vizEnabled;
this.isEnabled = instrumentationEnabled;
this.ui = options.ui;
// project constructor will set up bidirectional link
this.project = null;
this.instrumentations = {
init: options.initInstrumentation,
build: {
token: null,
node: null,
count: 0,
},
command: {
token: null,
node: null,
},
shutdown: {
token: null,
node: null,
},
};
this._heimdall = null;
this.config = getConfig();
if (!options.initInstrumentation && this.isEnabled()) {
this.instrumentations.init = {
token: null,
node: null,
};
this.start('init');
}
}
_buildSummary(tree, result, resultAnnotation) {
let buildSteps = 0;
let totalTime = 0;
let node;
let statName;
let statValue;
let nodeItr;
let statsItr;
let nextNode;
let nextStat;
for (nodeItr = tree.dfsIterator(); ; ) {
nextNode = nodeItr.next();
if (nextNode.done) {
break;
}
node = nextNode.value;
if (node.label.broccoliNode && !node.label.broccoliCachedNode) {
++buildSteps;
}
for (statsItr = node.statsIterator(); ; ) {
nextStat = statsItr.next();
if (nextStat.done) {
break;
}
statName = nextStat.value[0];
statValue = nextStat.value[1];
if (statName === 'time.self') {
totalTime += statValue;
}
}
}
let summary = {
build: {
type: resultAnnotation.type,
count: this.instrumentations.build.count,
outputChangedFiles: null,
},
platform: {
name: process.platform,
},
output: null,
totalTime,
buildSteps,
};
_getHardwareInfo(summary.platform);
if (result) {
summary.build.outputChangedFiles = result.outputChanges;
summary.output = result.directory;
}
if (resultAnnotation.type === 'rebuild') {
summary.build.primaryFile = resultAnnotation.primaryFile;
summary.build.changedFileCount = resultAnnotation.changedFiles.length;
summary.build.changedFiles = resultAnnotation.changedFiles.slice(0, 10);
}
return summary;
}
_initSummary(tree) {
const summary = {
totalTime: totalTime(tree),
platform: {
name: process.platform,
},
};
_getHardwareInfo(summary.platform);
return summary;
}
_commandSummary(tree, commandName, commandArgs) {
const summary = {
name: commandName,
args: commandArgs,
totalTime: totalTime(tree),
platform: {
name: process.platform,
},
};
_getHardwareInfo(summary.platform);
return summary;
}
_shutdownSummary(tree) {
const summary = {
totalTime: totalTime(tree),
platform: {
name: process.platform,
},
};
_getHardwareInfo(summary.platform);
return summary;
}
_instrumentationFor(name) {
let instr = this.instrumentations[name];
if (!instr) {
throw new Error(`No such instrumentation "${name}"`);
}
return instr;
}
_instrumentationTreeFor(name) {
return heimdallGraph.loadFromNode(this.instrumentations[name].node);
}
_invokeAddonHook(name, instrumentationInfo) {
if (this.project && this.project.addons.length) {
this.project.addons.forEach((addon) => {
if (typeof addon.instrumentation === 'function') {
addon.instrumentation(name, instrumentationInfo);
}
});
}
}
_writeInstrumentation(name, instrumentationInfo) {
if (!vizEnabled()) {
return;
}
let filename = `instrumentation.${name}`;
if (name === 'build') {
filename += `.${this.instrumentations.build.count}`;
}
filename = `${filename}.json`;
fs.writeJsonSync(filename, {
summary: instrumentationInfo.summary,
// we want to change this to tree, to be consistent with the hook, but first
// we must update broccoli-viz
// see see https://github.com/ember-cli/broccoli-viz/issues/35
nodes: instrumentationInfo.tree.toJSON().nodes,
});
}
start(name) {
if (!instrumentationEnabled(this.config)) {
return;
}
let instr = this._instrumentationFor(name);
this._heimdall = this._heimdall || require('heimdalljs');
if (instr.node) {
// don't leak nodes during build. We have already reported on this in the
// previous stopAndReport so no data is lost
instr.node.remove();
}
let token = this._heimdall.start({ name, emberCLI: true });
instr.token = token;
instr.node = this._heimdall.current;
}
stopAndReport(name) {
if (!instrumentationEnabled(this.config)) {
return;
}
let instr = this._instrumentationFor(name);
if (!instr.token) {
throw new Error(`Cannot stop instrumentation "${name}". It has not started.`);
}
try {
instr.token.stop();
} catch (e) {
this.ui.writeLine(chalk.red(`Error reporting instrumentation '${name}'.`));
logger.error(e.stack);
return;
}
let instrSummaryName = `_${name}Summary`;
if (!this[instrSummaryName]) {
throw new Error(`No summary found for "${name}"`);
}
let tree = this._instrumentationTreeFor(name);
let args = Array.prototype.slice.call(arguments, 1);
args.unshift(tree);
let instrInfo = {
summary: this[instrSummaryName].apply(this, args),
tree,
};
this._invokeAddonHook(name, instrInfo);
this._writeInstrumentation(name, instrInfo);
if (name === 'build') {
instr.count++;
}
}
}
function totalTime(tree) {
let totalTime = 0;
let nodeItr;
let node;
let statName;
let statValue;
let statsItr;
let nextNode;
let nextStat;
for (nodeItr = tree.dfsIterator(); ; ) {
nextNode = nodeItr.next();
if (nextNode.done) {
break;
}
node = nextNode.value;
for (statsItr = node.statsIterator(); ; ) {
nextStat = statsItr.next();
if (nextStat.done) {
break;
}
statName = nextStat.value[0];
statValue = nextStat.value[1];
if (statName === 'time.self') {
totalTime += statValue;
}
}
}
return totalTime;
}
// exported for testing
Instrumentation._enableFSMonitorIfInstrumentationEnabled = _enableFSMonitorIfInstrumentationEnabled;
module.exports = Instrumentation;