UNPKG

bem

Version:
670 lines (534 loc) 19.1 kB
'use strict'; var Q = require('q'), /* jshint -W098 */ COLORS = require('colors'), /* jshint +W098 */ BENCHMARK = require('benchmark'), FS = require('fs'), VM = require('vm'), PATH = require('path'), CP = require('child_process'), INHERIT = require('inherit'), LOGGER = require('./logger'), LEVEL = require('./level'), U = require('./util'), Table = require('cli-table'); /** * Benchmark - testing on BEMHTML templates performance between revisions */ var Benchmark = INHERIT({ __constructor: function(opts, args){ this.pathTmp = '.bem/cache/bench/'; this.pathSelf = 'current_state'; this.pathBenchmarks = 'benchmark.bundles'; this.treeishList = args['treeish-list'] || []; this.withoutCurrent = opts['no-wc']; this.rerun = opts.rerun; this.benchmarks = opts.benchmarks; this.techs = opts.techs; this.DELAY = opts.delay ? opts.delay : 20; this.WARMING_CYCLE = 100; this.ALLOWABLE_PERCENT = opts.rme ? opts.rme : 5; }, /** * Extract one revision to pathTmp folder * * @param {String} treeish Hash, HEAD, tag... * @return {Promise * String} Path to temporary revision */ exportGitRevision: function(treeish) { var def = Q.defer(), label = 'Export git revision [' + treeish.green + ']', archiveErr = '', extractErr = '', extract, archive; LOGGER.time(label); archive = CP.spawn('git', [ 'archive', '--format=tar', '--prefix=' + this.pathTmp + '/' + treeish + '/', treeish ]); extract = CP.spawn('tar', ['-x']); archive.stdout.on('data', function(data) { extract.stdin.write(data); }); archive.stderr.on('data', function(err) { archiveErr += err.toString().replace(/(\n)$/, ''); }); archive.on('close', function(code) { if (code !== 0) { LOGGER.error('exportGitRevision - '.blue + '[' + treeish.green + '] ' + archiveErr); def.reject(new Error(archiveErr)); } else { LOGGER.timeEnd(label); def.resolve(treeish); } extract.stdin.end(); }); extract.stderr.on('data', function(err) { extractErr += err.toString().replace(/(\n)$/, ''); }); extract.on('close', function(code) { if (code !== 0) { LOGGER.error('exportGitRevision - '.blue + '[' + treeish.green + '] ' + extractErr); def.reject(new Error(extractErr)); } }); return def.promise; }, /** * Copy current files state with exclude * * @return {Promise * String} Path to files */ exportWorkingCopy: function() { var self = this, label = 'Export working copy [' + self.pathSelf.green + ']', cmd = [ 'rsync', '-a', '--exclude=/.git', '--exclude=/.svn', '--exclude=/' + self.pathTmp, '.', self.pathTmp + self.pathSelf ].join(' '); LOGGER.time(label); return U.exec(cmd) .fail(function(err) { LOGGER.error('exportWorkingCopy'.blue); return Q.reject(err); }) .then(function() { LOGGER.timeEnd(label); return self.pathSelf; }); }, /** * Getting all paths to .bemjson.js file on needed level * * @param {String} treeishPath Checked out treeish path * @param {String} levelPath level path * @return {Array} Array of links */ getBenchmarksPaths: function(treeishPath, levelPath) { var self = this, level = LEVEL.createLevel(PATH.join(treeishPath, levelPath)), benchmarks; if (self.benchmarks && self.benchmarks.length) { // if -b flag detected then compare not all benchmarks benchmarks = U.arrayUnique(self.benchmarks) .map(function(b) { return U.bemParseKey(b); }); } else { // if -b flag not detected then all benchmarks benchmarks = level.getItemsByIntrospection() .filter(function(item) { return item.tech === 'bemjson.js'; }); } return benchmarks .map(function(benchmark) { return level.getPathByObj(benchmark, 'bemjson.js'); }); }, /** * Make one target and return all links on blocks * * @param {String} target Path to target * @return {Array} All links of target */ makeTarget: function(target) { var self = this, targets = [self.pathBenchmarks], label = '[' + target.green + '] has been assembled'; LOGGER.time(label); if (self.benchmarks && self.benchmarks.length) { targets = self.benchmarks .map(function(b) { return PATH.join(self.pathBenchmarks, b); }); } return U.exec('cd ' + PATH.join(self.pathTmp, target) + ' && npm install && ./node_modules/.bin/bem make ' + targets.join(' ')) .fail(function(err) { LOGGER.error(target.red + ' not make'); return Q.reject(err); }) .then(function() { LOGGER.timeEnd(label); }); }, /** * Make all targets and run benchmarks for each target * * @param {Array} targets List of targets * @return {Promise * Undefined} Object with tests results */ makeAll: function(targets) { return Q.all(targets.map(this.makeTarget.bind(this))) .then(function() {}); }, /** * Active waiting * * @param {Number} seconds Time in seconds to wait * @return {Promise * Undefined} */ activeWait: function(seconds) { if (seconds === 0) return; var dt = new Date().getTime(), title = seconds + 'sec'; LOGGER.info('Delay ' + title.red); while (dt + (seconds * 1000) > new Date().getTime()) {} }, /** * Run all benchmarks of the single target * * @param {Array} links Links to source bemjson files * @param {String} target Revision name * @return {Promise * Object} Object with tests results */ runBenchmark: function(links, target) { var self = this; return Q.all(links.map(function(link) { var def = Q.defer(), bemjson = FS.readFileSync(link , 'UTF-8'), bemhtml = link.replace(/.bemjson.js/, '.bemhtml.js'), bh = link.replace(/.bemjson.js/, '.bh.js'), context = VM.createContext({}), res = VM.runInContext(bemjson, context, link), name = link.match(/([^\/]+)$/)[0], nameShort = name.replace('.bemjson.js',''), suite = new BENCHMARK.Suite(), results = [], isBh = false, isBemhtml = false, label = '[' + target.green + ' => ' + nameShort.blue + '] has been tested'; LOGGER.time(label); if (U.isFile(bemhtml)) { if (!self.techs || self.techs.indexOf('bemhtml') !== -1) { bemhtml = require(bemhtml); isBemhtml = true; } } if (U.isFile(bh)) { if (!self.techs || self.techs.indexOf('bh') !== -1) { bh = require(bh); isBh = true; } } // Warming-up for the best results (function() { var i = self.WARMING_CYCLE; while (i--) { if (isBemhtml) bemhtml.BEMHTML.apply(res); if (isBh) bh.INST.apply(res); } })(); if (isBh) { suite.add(name + '(bh)'.underline, function() { bh.INST.apply(res); }); } if (isBemhtml) { suite.add(name + '(bemhtml)'.underline, function() { bemhtml.BEMHTML.apply(res); }); } suite .on('cycle', function(event) { results.push({ 'name' : String(event.target.name), 'hz' : Number(Math.round(event.target.hz) / 1000).toFixed(1), 'rme' : Number(event.target.stats.rme).toFixed(1), 'runs' : event.target.stats.sample.length, 'isSeparator' : isBemhtml && isBh }); }) .on('complete', function(event) { LOGGER.timeEnd(label); def.resolve(results); }) .on('error', function(err) { def.reject(err); }) .run({ 'async': false }); return def.promise; })) .then(function(rs) { var objs = []; rs.forEach(function(i) { i.forEach(function(j) { objs.push(j); }); }); return [target].concat(objs); }); }, /** * Extract all revisions and mark that it exist * * @return {Promise * Undefined} */ exportGitRevisions: function() { var self = this; return self.treeishList .map(function(treeish) { return self.exportGitRevision(treeish); }); }, /** * Copy reference benchmarks from specified source into every checked out revision * * @param {String[]} targets Array of target names * @param {String} source Path to benchmarks source * @return {Promise * Undefined} */ cloneBenchmarks: function(source, targets) { targets = [].concat(targets); var self = this, search = targets.indexOf(source), onFail = function(err) { LOGGER.error('cloneBenchmarks - '.blue + err); return Q.reject(err); }; if (search !== -1) { targets.splice(search, 1); } return Q.all(targets.map(function(target) { var cmd = 'rm -rf ' + PATH.join(self.pathTmp, target, self.pathBenchmarks); return U.exec(cmd) .fail(onFail) .then(function() { var cmd = [ 'cp', '-R', PATH.join(self.pathTmp, source, self.pathBenchmarks + '*'), PATH.join(self.pathTmp, target, self.pathBenchmarks) ].join(' '); return U.exec(cmd) .fail(onFail); }); })); }, /** * Return latest treeish compared at date * * @return {String} Latest treeish */ getLatestTreeish: function() { var self = this; if (!self.withoutCurrent) { LOGGER.info('Latest revision is - ', self.pathSelf.magenta); return Q.resolve(self.pathSelf); } return Q.all(self.treeishList .map(function(treeish) { /* jshint -W109 */ var cmd = "git show " + treeish + " | grep Date | awk -F': ' '{print $2}'"; /* jshint +W109 */ return U.exec(cmd, null, true) .then(function(output) { return { treeish: treeish, date: output.replace('\n', '') }; }); }) ) .then(function(dates) { var maxTime = 0, lastTreeish; dates.forEach(function(dt) { var time = new Date(dt.date).getTime(); if (time > maxTime) { maxTime = time; lastTreeish = dt.treeish; } }); LOGGER.info('Latest revision is - ' + lastTreeish.magenta); return lastTreeish; }); }, /** * Sort results by command line order * * @param {Object} res Results data * @param {String[]} targets Array of target names * @return {Array} Sorted results data */ sortResult: function(res, targets) { var sorted = []; for (var i = 0; i < res.length; i++) { for (var j = 0; j < res.length; j++) { if (res[j][0] === targets[i]) { sorted.push(res[j]); break; } } } return sorted; }, /** * Construct cli table object with benchmarks results * * @param {Object} results Object with test results * @param {String[]} targets Array of target names * @return {Object} cli table object */ getResultsTable: function(results, targets) { var sortedRes = this.sortResult(results, targets), header = [], data = [], rme = [], isSeparator, name, perc, t; header.push( '№'.magenta, 'benchmark'.yellow.underline); sortedRes.forEach(function(item) { header.push(item[0].blue + '(hz/rme)'.cyan); }); for (var i = 1; i < sortedRes[0].length; i += 1) { name = undefined; t = []; rme = []; isSeparator = false; for (var j = 0; j < sortedRes.length; j++) { if (!name) name = sortedRes[j][i].name; if (sortedRes[j][i]) { t.push('[' + sortedRes[j][i].hz.green + '] ±' + sortedRes[j][i].rme.magenta + '%'); rme.push(Number(sortedRes[j][i].rme)); if (sortedRes[j][i].isSeparator === true) { isSeparator = true; } } else { t.push('none'); } } if ((i+1) % 2 === 0 && isSeparator) { data.push([]); } var max = Math.max.apply(null, rme), min = Math.min.apply(null, rme); if ((max - min) < this.ALLOWABLE_PERCENT) { perc = 'stable'.yellow.underline; } else { perc = 'unstable'.red.underline; } data.push([i, name.replace('.bemjson.js', '')] .concat(t) .concat(perc)); } header.push('RME stat'.magenta); var table = new Table({ head: header, style: { compact: true, 'padding-left': 1, 'padding-right': 1 } }); data.forEach(function(row) { table.push(row); }); return table; }, /** * Cleaning all repo from temporary folder * * @return {Promise * Undefined} */ cleanTempDir: function() { var cmd = 'rm -rf ./' + this.pathTmp + '*', label = 'TMP folder has been cleaned'; LOGGER.info('Cleaning a TMP folder'); LOGGER.time(label); return U.exec(cmd) .fin(function() { LOGGER.timeEnd(label); }); }, /** * Main flow * * @return {Promise} Promise of benchmarks finish */ start: function() { var self = this, bcsInclude = 'Include ' + String(this.pathSelf).magenta, bcsExclude = 'Exclude ' + String(this.pathSelf).magenta, targets = [].concat(self.treeishList); LOGGER.time('All time'); if (!self.withoutCurrent) { LOGGER.info(bcsInclude); targets.push(self.pathSelf); } else { LOGGER.info(bcsExclude); } return Q.resolve() .then(function() { if (self.rerun) { // without make LOGGER.info('[NO MAKE]'.magenta + ' mode'); return Q.resolve(); } else { // export and make revisions return U.mkdirp(self.pathTmp) .then(function() { return self.cleanTempDir(); }) .then(function() { var exports = self.exportGitRevisions(); if (!self.withoutCurrent) { exports.push(self.exportWorkingCopy()); } return Q.all(exports) .then(function() { return self.getLatestTreeish() .then(function(rev) { return self.cloneBenchmarks(rev, targets); }); }) .then(function() { LOGGER.info('Make...'); return self.makeAll(targets); }); }); } }) .then(function() { LOGGER.info('Benchmark...'); return Q.all(targets .map(function(target) { var links = self.getBenchmarksPaths( PATH.join(self.pathTmp, target), self.pathBenchmarks); self.activeWait(self.DELAY); return self.runBenchmark(links, target); }) ); }) .then(function(res) { LOGGER.timeEnd('All time'); return self.getResultsTable(res, targets); }); } }); /** * Create instance on Benchmark and share it * * @param {Array} opts Options from COA * @param {Array} args Arguments from COA * @return {Object} Benchmark instance */ module.exports = function(opts, args) { return new Benchmark(opts, args); };