1 | 'use strict';
|
2 |
|
3 | var 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 |
|
20 |
|
21 | var 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 |
|
41 |
|
42 |
|
43 |
|
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 |
|
100 |
|
101 |
|
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 |
|
133 |
|
134 |
|
135 |
|
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 |
|
199 |
|
200 |
|
201 |
|
202 |
|
203 | getLevel: function(path) {
|
204 | return path.split(':')[0];
|
205 | },
|
206 |
|
207 | |
208 |
|
209 |
|
210 |
|
211 |
|
212 |
|
213 | getBlock: function(path) {
|
214 | return path.split(':')[1];
|
215 | },
|
216 |
|
217 | |
218 |
|
219 |
|
220 |
|
221 |
|
222 |
|
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 |
|
260 |
|
261 |
|
262 |
|
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 |
|
315 |
|
316 |
|
317 |
|
318 |
|
319 | makeAll: function(targets) {
|
320 | return Q.all(targets
|
321 | .map(this.makeTarget.bind(this))).thenResolve();
|
322 | },
|
323 |
|
324 | |
325 |
|
326 |
|
327 |
|
328 |
|
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 |
|
344 |
|
345 |
|
346 |
|
347 |
|
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 |
|
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 |
|
451 |
|
452 |
|
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 |
|
467 |
|
468 |
|
469 |
|
470 |
|
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 |
|
512 |
|
513 |
|
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 |
|
562 |
|
563 |
|
564 |
|
565 |
|
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 |
|
586 |
|
587 |
|
588 |
|
589 |
|
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 |
|
670 |
|
671 |
|
672 |
|
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 |
|
688 |
|
689 |
|
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 |
|
708 |
|
709 |
|
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 |
|
732 | LOGGER.info('[NO MAKE]'.magenta + ' mode');
|
733 | return Q.resolve();
|
734 | }
|
735 | else {
|
736 |
|
737 |
|
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 |
|
796 |
|
797 |
|
798 |
|
799 |
|
800 |
|
801 | module.exports = function(opts, args) {
|
802 | return new Benchmark(opts, args);
|
803 | };
|