UNPKG

24.8 kBJavaScriptView Raw
1'use strict';
2
3var Q = require('q'),
4 COLORS = require('colors'),
5 BENCHMARK = require('benchmark'),
6 FS = require('fs'),
7 VM = require('vm'),
8 PATH = require('path'),
9 CP = require('child_process'),
10 INHERIT = require('inherit'),
11 LOGGER = require('./logger'),
12 LEVEL = require('./level'),
13 U = require('./util'),
14 UTIL = require('util'),
15
16 Table = require('cli-table');
17
18/**
19 * Benchmark - testing on BEMHTML templates performance between revisions
20 */
21var Benchmark = INHERIT({
22
23 __constructor: function(opts, args){
24
25 this.pathTmp = '.bem/cache/bench/';
26 this.pathSelf = 'current_state';
27 this.benchmarks = args.benchmarks;
28 this.withoutCurrent = opts['no-wc'];
29 this.rerun = opts.rerun;
30 this.treeishList = opts['treeish-list'] || [];
31 this.techs = opts.techs;
32
33 this.DELAY = opts.delay ? opts.delay : 20;
34 this.WARMING_CYCLE = 100;
35 this.ALLOWABLE_PERCENT = opts.rme ? opts.rme : 5;
36
37 },
38
39 /**
40 * Extract one revision to pathTmp folder
41 *
42 * @param {String} treeish Hash, HEAD, tag...
43 * @return {Promise * String} Path to temporary revision
44 */
45 exportGitRevision: function(treeish) {
46
47 var def = Q.defer(),
48 label = 'Export git revision [' + treeish.green + ']',
49 archiveErr = '',
50 extractErr = '',
51 extract,
52 archive;
53
54 LOGGER.time(label);
55
56 archive = CP.spawn('git', [
57 'archive',
58 '--format=tar',
59 '--prefix=' + this.pathTmp + '/' + treeish + '/',
60 treeish
61 ]);
62 extract = CP.spawn('tar', ['-x']);
63
64 archive.stdout.on('data', function(data) {
65 extract.stdin.write(data);
66 });
67
68 archive.stderr.on('data', function(err) {
69 archiveErr += err.toString().replace(/(\n)$/, '');
70 });
71
72 archive.on('close', function(code) {
73 if (code !== 0) {
74 LOGGER.error('exportGitRevision - '.blue + '[' + treeish.green + '] ' + archiveErr);
75 def.reject(new Error(archiveErr));
76 } else {
77 LOGGER.timeEnd(label);
78 def.resolve(treeish);
79 }
80 extract.stdin.end();
81 });
82
83 extract.stderr.on('data', function(err) {
84 extractErr += err.toString().replace(/(\n)$/, '');
85 });
86
87 extract.on('close', function(code) {
88 if (code !== 0) {
89 LOGGER.error('exportGitRevision - '.blue + '[' + treeish.green + '] ' + extractErr);
90 def.reject(new Error(extractErr));
91 }
92 });
93
94 return def.promise;
95
96 },
97
98 /**
99 * Copy current files state with exclude
100 *
101 * @return {Promise * String} Path to files
102 */
103 exportWorkingCopy: function() {
104
105 var self = this,
106 label = 'Export working copy [' + self.pathSelf.green + ']',
107 cmd = [
108 'rsync',
109 '-a',
110 '--exclude=/.git',
111 '--exclude=/.svn',
112 '--exclude=/' + self.pathTmp,
113 '.',
114 self.pathTmp + self.pathSelf
115 ].join(' ');
116
117 LOGGER.time(label);
118
119 return U.exec(cmd)
120 .fail(function(err) {
121 LOGGER.error('exportWorkingCopy'.blue);
122 return Q.reject(err);
123 })
124 .then(function() {
125 LOGGER.timeEnd(label);
126 return self.pathSelf;
127 });
128
129 },
130
131 /**
132 * Getting all paths to .bemjson.js file for level
133 *
134 * @param {String} treeishPath Checked out treeish path
135 * @return {Array} Array of links
136 */
137 getBenchmarksPaths: function(treeishPath) {
138
139 var self = this,
140 benchmarks;
141
142 if (self.benchmarks && self.benchmarks.length) {
143
144 benchmarks = self.benchmarks
145 .reduce(function(prev, curr) {
146 var level = self.getLevel(curr);
147 if (level && !~prev.indexOf(level)) {
148 return prev.concat(level);
149 }
150 return prev;
151 }, [])
152 .map(function(level) {
153 return {
154 level: level,
155 bemjsonPaths: []
156 };
157 });
158
159 return Q.all(self.benchmarks
160 .map(function(item) {
161 var level = self.getLevel(item),
162 block = self.getBlock(item);
163
164 return self.getBemjsonPathsByLevel(treeishPath, level, block)
165 .then(function(paths) {
166 benchmarks.forEach(function(subItem) {
167 if (subItem.level === level) {
168 paths.forEach(function(path) {
169 if(!~subItem.bemjsonPaths.indexOf(path)) {
170 subItem.bemjsonPaths.push(path);
171 }
172 });
173 }
174 });
175 });
176 }))
177 .then(function() {
178 return benchmarks;
179 });
180
181 }
182
183 return Q.all(self.getBenchLevelsByPath(treeishPath)
184 .map(function(level) {
185 return self.getBemjsonPathsByLevel(treeishPath, level)
186 .then(function(paths) {
187 return {
188 'level': level,
189 'bemjsonPaths': paths
190 };
191 });
192 }));
193
194 },
195
196
197 /**
198 * Return level for path
199 *
200 * @param {String} path path to block or level, like 'desktop.benchmarks:block_name'
201 * @return {String} level path, like - 'desktop.benchmarks'
202 */
203 getLevel: function(path) {
204 return path.split(':')[0];
205 },
206
207 /**
208 * Return block name for path
209 *
210 * @param {String} path path to block, like 'desktop.benchmarks:block_name'
211 * @return {String} block name, like - 'block_name'
212 */
213 getBlock: function(path) {
214 return path.split(':')[1];
215 },
216
217 /**
218 * Return all full paths to bemjson.js files on specific level
219 *
220 * @param {String} projectPath path to project root
221 * @param {String} level level path
222 * @return {String[]} full bemjson paths
223 */
224 getBemjsonPathsByLevel: function(projectPath, level, block) {
225 var levelInstance = LEVEL.createLevel(PATH.join(projectPath, level));
226
227 return Q.when(levelInstance.scanFiles())
228 .then(function(files) {
229 return Object.keys(files)
230 .reduce(function(paths, treeNode) {
231 var bemjsonItems = files[treeNode]['bemjson.js'] || [];
232 return bemjsonItems.concat(paths);
233 }, []);
234 })
235 .then(function(paths) {
236 var filtered = [];
237
238 if (block) {
239 paths.every(function(path) {
240 var currentBlock = path.file.replace('.bemjson.js', '');
241 if (currentBlock === block) {
242 filtered = [path];
243 return false;
244 }
245 return true;
246 });
247 } else {
248 filtered = paths;
249 }
250
251 return filtered
252 .map(function(path) {
253 return path.absPath;
254 });
255 });
256 },
257
258 /**
259 * Make one target and return all links on blocks
260 *
261 * @param {String} target Path to target
262 * @return {Array} All links of target
263 */
264 makeTarget: function(target) {
265
266 var self = this,
267 targets = self.getBenchLevelsByPath(PATH.join(self.pathTmp, target)),
268 label = '[' + target.green + '] has been assembled',
269 script = require(PATH.join(process.cwd(), self.pathTmp, target, 'package.json')).scripts,
270 defaultMake = 'npm install && bem make',
271 make = null,
272 envClone = Object.create(process.env);
273
274 if (self.benchmarks && self.benchmarks.length) {
275 targets = self.benchmarks
276 .map(function(target) {
277 return target.replace(':', '/');
278 });
279 }
280
281 if(script && script['bem-bench-build']) {
282 make = script['bem-bench-build'];
283 if (/\$targets/.test(make)) {
284 if (!self.benchmarks) {
285 LOGGER.warn('$targets'.blue + ' not been defined through arguments');
286 LOGGER.warn('$targets'.blue + ' change to ' + targets);
287 }
288 make = make.replace('$targets', targets.join(' '));
289 }
290 } else {
291 make = defaultMake + ' ' + targets.join(' ');
292 }
293
294 LOGGER.info('MAKE STRING [' + target.green + ']:\n', make.replace(/&&/gi, '&&\n'.green).underline);
295
296 envClone.PATH = PATH.join(process.cwd(), 'node_modules/.bin') + ':' + process.env.PATH;
297
298 LOGGER.time(label);
299 return U.exec(make, {
300 cwd: PATH.join(self.pathTmp, target),
301 env: process.env
302 })
303 .fail(function(err) {
304 LOGGER.error(target.red + ' not make');
305 return Q.reject(err);
306 })
307 .then(function() {
308 LOGGER.timeEnd(label);
309 });
310
311 },
312
313 /**
314 * Make all targets and run benchmarks for each target
315 *
316 * @param {Array} targets List of targets
317 * @return {Promise * Undefined} Object with tests results
318 */
319 makeAll: function(targets) {
320 return Q.all(targets
321 .map(this.makeTarget.bind(this))).thenResolve();
322 },
323
324 /**
325 * Active waiting
326 *
327 * @param {Number} seconds Time in seconds to wait
328 * @return {Promise * Undefined}
329 */
330 activeWait: function(seconds) {
331
332 if (seconds === 0) return;
333
334 var dt = new Date().getTime(),
335 title = seconds + 'sec';
336
337 LOGGER.info('Delay ' + title.red);
338 while (dt + (seconds * 1000) > new Date().getTime()) {}
339
340 },
341
342 /**
343 * Run all benchmarks of the single target
344 *
345 * @param {Object} links Links to source bemjson files
346 * @param {String} target Revision name
347 * @return {Promise * Object} Object with tests results
348 */
349 runBenchmark: function(links, target) {
350
351 var self = this;
352
353 return Q.all(links
354 .map(function(level) {
355
356 return Q.all(level.bemjsonPaths
357 .map(function(link) {
358
359 var def = Q.defer(),
360 bemjson = FS.readFileSync(link , 'UTF-8'),
361 res = VM.runInContext(bemjson, VM.createContext({}), link),
362 bemhtml = link.replace(/.bemjson.js/, '.bemhtml.js'),
363 bh = link.replace(/.bemjson.js/, '.bh.js'),
364 name = link.match(/([^\/]+)$/)[0],
365 nameShort = name.replace('.bemjson.js',''),
366 fullName = PATH.join(level.level, nameShort),
367 suite = new BENCHMARK.Suite(),
368 results = [],
369 isBh = false,
370 isBemhtml = false,
371
372 label = '[' + target.green + ' => ' + fullName.magenta + '] has been tested';
373
374
375 LOGGER.time(label);
376
377 if (U.isFile(bemhtml)) {
378 if (!self.techs || self.techs.indexOf('bemhtml') !== -1) {
379 bemhtml = require(bemhtml);
380 isBemhtml = true;
381 }
382 }
383
384 if (U.isFile(bh)) {
385 if (!self.techs || self.techs.indexOf('bh') !== -1) {
386 bh = require(bh);
387 isBh = true;
388 }
389 }
390
391 // Warming-up for the best results
392 (function() {
393 var i = self.WARMING_CYCLE;
394 while (i--) {
395 if (isBemhtml) bemhtml.BEMHTML.apply(res);
396 if (isBh) bh.INST.apply(res);
397 }
398 })();
399
400 if (isBh) {
401 suite.add(fullName + '(bh)'.underline, function() {
402 bh.INST.apply(res);
403 });
404 }
405
406 if (isBemhtml) {
407 suite.add(fullName + '(bemhtml)'.underline, function() {
408 bemhtml.BEMHTML.apply(res);
409 });
410 }
411
412 suite
413 .on('cycle', function(event) {
414 results.push({
415 name : String(event.target.name),
416 hz : Number(Math.round(event.target.hz) / 1000).toFixed(1),
417 rme : Number(event.target.stats.rme).toFixed(1),
418 runs : event.target.stats.sample.length,
419 isSeparator : isBemhtml && isBh
420 });
421 })
422 .on('complete', function(event) {
423 LOGGER.timeEnd(label);
424 def.resolve(results);
425 })
426 .on('error', function(err) {
427 def.reject(err);
428 })
429 .run({ 'async': false });
430
431 return def.promise;
432 }));
433
434 }))
435 .then(function(rs) {
436 var objs = [target];
437
438 rs.forEach(function(i) {
439 i.forEach(function(j) {
440 objs = objs.concat(j);
441 });
442 });
443
444 return objs;
445 });
446
447 },
448
449 /**
450 * Extract all revisions and mark that it exist
451 *
452 * @return {Promise * Undefined}
453 */
454 exportGitRevisions: function() {
455
456 var self = this;
457
458 return self.treeishList
459 .map(function(treeish) {
460 return self.exportGitRevision(treeish);
461 });
462
463 },
464
465 /**
466 * Copy reference benchmarks from specified source into every checked out revision
467 *
468 * @param {String[]} targets Array of target names
469 * @param {String} source Path to benchmarks source
470 * @return {Promise * Undefined}
471 */
472 cloneBenchmarks: function(source, targets) {
473
474 targets = [].concat(targets);
475
476 var self = this,
477 search = targets.indexOf(source),
478 levels = this.getBenchLevelsByPath(PATH.join(this.pathTmp, source)),
479
480 onFail = function(err) {
481 LOGGER.error('cloneBenchmarks - '.blue + err);
482 return Q.reject(err);
483 };
484
485 if (search !== -1) {
486 targets.splice(search, 1);
487 }
488
489 return Q.all(targets.map(function(target) {
490
491 return Q.all(levels
492 .map(function(level) {
493 var cmd = 'rm -rf ' + PATH.join(self.pathTmp, target, level);
494 return U.exec(cmd)
495 .fail(onFail)
496 .then(function() {
497
498 var dest = PATH.join(self.pathTmp, target),
499 cmd = ['cp', '-R', PATH.join(self.pathTmp, source, level), dest].join(' ');
500
501 return U.exec(cmd)
502 .fail(onFail);
503
504 });
505 }));
506 }));
507
508 },
509
510 /**
511 * Return latest treeish compared at date
512 *
513 * @return {String} Latest treeish
514 */
515 getLatestTreeish: function() {
516
517 var self = this;
518
519 if (!self.withoutCurrent) {
520 LOGGER.info('Latest revision is - ', self.pathSelf.magenta);
521 return Q.resolve(self.pathSelf);
522 }
523
524 return Q.all(self.treeishList
525 .map(function(treeish) {
526
527 var cmd = "git show " + treeish + " | grep Date | awk -F': ' '{print $2}'";
528
529 return U.exec(cmd, null, true)
530 .then(function(output) {
531 return {
532 treeish: treeish,
533 date: output.replace('\n', '')
534 };
535 });
536
537 })
538 )
539 .then(function(dates) {
540
541 var maxTime = 0,
542 lastTreeish;
543
544 dates.forEach(function(dt) {
545 var time = new Date(dt.date).getTime();
546 if (time > maxTime) {
547 maxTime = time;
548 lastTreeish = dt.treeish;
549 }
550 });
551
552 LOGGER.info('Latest revision is - ' + lastTreeish.magenta);
553
554 return lastTreeish;
555
556 });
557
558 },
559
560 /**
561 * Sort results by command line order
562 *
563 * @param {Object} res Results data
564 * @param {String[]} targets Array of target names
565 * @return {Array} Sorted results data
566 */
567 sortResult: function(res, targets) {
568
569 var sorted = [];
570
571 for (var i = 0; i < res.length; i++) {
572 for (var j = 0; j < res.length; j++) {
573 if (res[j][0] === targets[i]) {
574 sorted.push(res[j]);
575 break;
576 }
577 }
578 }
579
580 return sorted;
581
582 },
583
584 /**
585 * Construct cli table object with benchmarks results
586 *
587 * @param {Object} results Object with test results
588 * @param {String[]} targets Array of target names
589 * @return {Object} cli table object
590 */
591 getResultsTable: function(results, targets) {
592
593 var sortedRes = this.sortResult(results, targets),
594 header = [],
595 data = [],
596 rme = [],
597 isSeparator,
598 name,
599 perc,
600 t;
601
602 header.push(
603 '№'.magenta,
604 'benchmark'.yellow.underline);
605
606 sortedRes.forEach(function(item) {
607 header.push(item[0].blue + '(hz/rme)'.cyan);
608 });
609
610 for (var i = 1; i < sortedRes[0].length; i += 1) {
611 name = undefined;
612 t = [];
613 rme = [];
614 isSeparator = false;
615
616 for (var j = 0; j < sortedRes.length; j++) {
617 if (!name) name = sortedRes[j][i].name;
618
619 if (sortedRes[j][i]) {
620 t.push('[' + sortedRes[j][i].hz.green + '] ±' + sortedRes[j][i].rme.magenta + '%');
621 rme.push(Number(sortedRes[j][i].rme));
622 if (sortedRes[j][i].isSeparator === true) {
623 isSeparator = true;
624 }
625 } else {
626 t.push('none');
627 }
628 }
629
630 if ((i+1) % 2 === 0 && isSeparator) {
631 data.push([]);
632 }
633
634 var max = Math.max.apply(null, rme),
635 min = Math.min.apply(null, rme);
636
637 if ((max - min) < this.ALLOWABLE_PERCENT) {
638 perc = 'stable'.yellow.underline;
639 } else {
640 perc = 'unstable'.red.underline;
641 }
642
643 data.push([i, name.replace('.bemjson.js', '')]
644 .concat(t)
645 .concat(perc));
646
647 }
648
649 header.push('RME stat'.magenta);
650
651 var table = new Table({
652 head: header,
653 style: {
654 compact: true,
655 'padding-left': 1,
656 'padding-right': 1
657 }
658 });
659
660 data.forEach(function(row) {
661 table.push(row);
662 });
663
664 return table;
665
666 },
667
668 /**
669 * Get all levels of benchmarks tech
670 *
671 * @param {String} path to level
672 * @return {Array} array of levels
673 */
674 getBenchLevelsByPath: function(path) {
675 var level = LEVEL.createLevel(path);
676
677 return level.getItemsByIntrospection()
678 .filter(function(item) {
679 return item.tech === 'benchmarks';
680 })
681 .map(function(item) {
682 return level.getRelByObj(item) + item.suffix;
683 });
684 },
685
686 /**
687 * Cleaning all repo from temporary folder
688 *
689 * @return {Promise * Undefined}
690 */
691 cleanTempDir: function() {
692
693 var cmd = 'rm -rf ./' + this.pathTmp + '*',
694 label = 'TMP folder has been cleaned';
695
696 LOGGER.info('Cleaning a TMP folder');
697 LOGGER.time(label);
698
699 return U.exec(cmd)
700 .fin(function() {
701 LOGGER.timeEnd(label);
702 });
703
704 },
705
706 /**
707 * Main flow
708 *
709 * @return {Promise} Promise of benchmarks finish
710 */
711 start: function() {
712
713 var self = this,
714 bcsInclude = 'Include ' + String(self.pathSelf).magenta,
715 bcsExclude = 'Exclude ' + String(self.pathSelf).magenta,
716 targets = [].concat(self.treeishList);
717
718 LOGGER.time('All time');
719
720 if (!self.withoutCurrent) {
721 LOGGER.info(bcsInclude);
722 targets.push(self.pathSelf);
723 } else {
724 LOGGER.info(bcsExclude);
725 }
726
727 return Q.resolve()
728 .then(function() {
729
730 if (self.rerun) {
731 // without make
732 LOGGER.info('[NO MAKE]'.magenta + ' mode');
733 return Q.resolve();
734 }
735 else {
736
737 // export and make revisions
738 return U.mkdirp(self.pathTmp)
739 .then(function() {
740 return self.cleanTempDir();
741 })
742 .then(function() {
743
744 var exports = self.exportGitRevisions();
745 if (!self.withoutCurrent) {
746 exports.push(self.exportWorkingCopy());
747 }
748
749 LOGGER.info('Export...');
750 return Q.all(exports)
751 .then(function() {
752 LOGGER.info('Export end');
753 return self.getLatestTreeish()
754 .then(function(rev) {
755 LOGGER.info('Cloning benchmarks');
756 return self.cloneBenchmarks(rev, targets);
757 });
758 })
759 .then(function() {
760 LOGGER.info('Make...');
761 return self.makeAll(targets);
762 });
763
764 });
765
766 }
767
768 })
769 .then(function() {
770 LOGGER.info('Benchmark...');
771
772 return Q.all(targets
773 .map(function(target) {
774
775 return self.getBenchmarksPaths(PATH.join(self.pathTmp, target))
776 .then(function(links) {
777 self.activeWait(self.DELAY);
778 return self.runBenchmark(links, target);
779 });
780
781 })
782 );
783
784 })
785 .then(function(res) {
786 LOGGER.timeEnd('All time');
787 return self.getResultsTable(res, targets);
788 });
789
790 }
791
792});
793
794/**
795 * Create instance on Benchmark and share it
796 *
797 * @param {Array} opts Options from COA
798 * @param {Array} args Arguments from COA
799 * @return {Object} Benchmark instance
800 */
801module.exports = function(opts, args) {
802 return new Benchmark(opts, args);
803};