1 | 'use strict';
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 | const fs = require('fs');
|
8 | const path = require('path');
|
9 | const util = require('util');
|
10 | const File = require('vinyl');
|
11 | const ansi = require('ansi');
|
12 | const Emitter = require('component-emitter');
|
13 | const Benchmark = require('benchmark');
|
14 | const define = require('define-property');
|
15 | const reader = require('file-reader');
|
16 | const isGlob = require('is-glob');
|
17 | const typeOf = require('kind-of');
|
18 | const merge = require('mixin-deep');
|
19 | const glob = require('resolve-glob');
|
20 | const log = require('log-utils');
|
21 | const mm = require('micromatch');
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 | const utils = require('./lib/utils');
|
28 | const cursor = ansi(process.stdout);
|
29 | const colors = log.colors;
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 | function Benchmarked(options) {
|
42 | if (!(this instanceof Benchmarked)) {
|
43 | return new Benchmarked(options);
|
44 | }
|
45 | Emitter.call(this);
|
46 | this.options = Object.assign({}, options);
|
47 | this.results = [];
|
48 | this.defaults(this);
|
49 | }
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 | util.inherits(Benchmarked, Emitter);
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 | Benchmarked.prototype.defaults = function(benchmarked) {
|
62 | this.fixtures = {
|
63 | files: [],
|
64 | cache: {},
|
65 | toFile: function(file) {
|
66 | const str = fs.readFileSync(file.path, 'utf8');
|
67 | file.content = reader.file(file);
|
68 | file.bytes = util.format('(%d bytes)', str.length);
|
69 | }
|
70 | };
|
71 |
|
72 | this.code = {
|
73 | files: [],
|
74 | cache: {},
|
75 | toFile: function(file) {
|
76 | file.run = require(file.path);
|
77 | }
|
78 | };
|
79 |
|
80 | if (typeof this.options.format === 'function') {
|
81 | this.format = this.options.format;
|
82 | }
|
83 | if (this.options.fixtures) {
|
84 | this.addFixtures(this.options.fixtures);
|
85 | }
|
86 | if (this.options.code) {
|
87 | this.addCode(this.options.code);
|
88 | }
|
89 | };
|
90 |
|
91 |
|
92 |
|
93 |
|
94 |
|
95 |
|
96 |
|
97 | Benchmarked.prototype.format = function(benchmark) {
|
98 | return ' ' + benchmark;
|
99 | };
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 |
|
107 |
|
108 | Benchmarked.prototype.toFile = function(type, filepath, options) {
|
109 | const opts = merge({cwd: this.cwd}, this.options, options);
|
110 | let file = new File({path: path.resolve(opts.cwd, filepath)});
|
111 |
|
112 | file.key = utils.setKey(file, opts);
|
113 | file.inspect = function() {
|
114 | return '<' + utils.toTitle(type) + ' ' + this.key + '"' + this.relative + '">';
|
115 | };
|
116 |
|
117 | const fn = opts.toFile || this[type].toFile;
|
118 | const res = fn.call(this, file);
|
119 | if (utils.isFile(res)) {
|
120 | file = res;
|
121 | }
|
122 | return file;
|
123 | };
|
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 |
|
131 |
|
132 | Benchmarked.prototype.filter = function(type, patterns, options) {
|
133 | if (typeof patterns === 'undefined') {
|
134 | patterns = '*';
|
135 | }
|
136 |
|
137 | const isMatch = mm.matcher(patterns, options);
|
138 | const results = [];
|
139 |
|
140 | for (let file of this.fixtures.files) {
|
141 | if (isMatch(file.basename)) {
|
142 | file.suite = this.addSuite(file, options);
|
143 | results.push(file);
|
144 | }
|
145 | }
|
146 | return results;
|
147 | };
|
148 |
|
149 |
|
150 |
|
151 |
|
152 |
|
153 |
|
154 |
|
155 |
|
156 | Benchmarked.prototype.match = function(type, patterns, options) {
|
157 | return mm.matchKeys(this[type].files, patterns, options);
|
158 | };
|
159 |
|
160 |
|
161 |
|
162 |
|
163 |
|
164 |
|
165 |
|
166 |
|
167 | Benchmarked.prototype.addFile = function(type, file, options) {
|
168 | if (isGlob(file) || Array.isArray(file)) {
|
169 | return this.addFiles.apply(this, arguments);
|
170 | }
|
171 |
|
172 | if (typeof file === 'string') {
|
173 | file = this.toFile(type, file, options);
|
174 | }
|
175 |
|
176 | if (!utils.isFile(file)) {
|
177 | throw new Error('expected "file" to be a vinyl file object');
|
178 | }
|
179 |
|
180 | this[type].cache[file.path] = file;
|
181 | this[type].files.push(file);
|
182 | return this;
|
183 | };
|
184 |
|
185 |
|
186 |
|
187 |
|
188 |
|
189 |
|
190 |
|
191 |
|
192 | Benchmarked.prototype.addFiles = function(type, files, options) {
|
193 | const opts = merge({cwd: this.cwd}, this.options, options);
|
194 | switch (typeOf(files)) {
|
195 | case 'string':
|
196 | if (isGlob(files)) {
|
197 | this.addFiles(type, glob.sync(files, opts), options);
|
198 | } else {
|
199 | this.addFile(type, files, options);
|
200 | }
|
201 | break;
|
202 | case 'array':
|
203 | this.addFilesArray(type, files, options);
|
204 | break;
|
205 | case 'object':
|
206 | this.addFilesObject(type, files, options);
|
207 | break;
|
208 | default: {
|
209 | throw new TypeError('cannot load files: ', util.inspect(arguments));
|
210 | }
|
211 | }
|
212 | return this;
|
213 | };
|
214 |
|
215 |
|
216 |
|
217 |
|
218 |
|
219 |
|
220 |
|
221 |
|
222 |
|
223 | Benchmarked.prototype.addFilesArray = function(type, files, options) {
|
224 | for (let file of files) {
|
225 | this.addFile(type, file, options);
|
226 | }
|
227 | return this;
|
228 | };
|
229 |
|
230 |
|
231 |
|
232 |
|
233 |
|
234 |
|
235 |
|
236 | Benchmarked.prototype.addFilesObject = function(type, files, options) {
|
237 | for (let key in Object.keys(files)) {
|
238 | this.addFile(type, files[key], options);
|
239 | }
|
240 | return this;
|
241 | };
|
242 |
|
243 |
|
244 |
|
245 |
|
246 |
|
247 |
|
248 |
|
249 | Benchmarked.prototype.addFixture = function(file, options) {
|
250 | this.addFile('fixtures', file, options);
|
251 | return this;
|
252 | };
|
253 |
|
254 |
|
255 |
|
256 |
|
257 |
|
258 |
|
259 |
|
260 |
|
261 |
|
262 |
|
263 |
|
264 |
|
265 | Benchmarked.prototype.addFixtures = function(files, options) {
|
266 | this.addFiles('fixtures', files, options);
|
267 | return this;
|
268 | };
|
269 |
|
270 |
|
271 |
|
272 |
|
273 |
|
274 |
|
275 |
|
276 |
|
277 |
|
278 |
|
279 |
|
280 |
|
281 | Benchmarked.prototype.addCode = function(file, options) {
|
282 | this.addFiles('code', file, options);
|
283 | return this;
|
284 | };
|
285 |
|
286 |
|
287 |
|
288 |
|
289 |
|
290 |
|
291 |
|
292 |
|
293 | Benchmarked.prototype.addSuite = function(fixture, options) {
|
294 | var files = this.code.files;
|
295 | var opts = this.options;
|
296 | var format = this.format;
|
297 |
|
298 |
|
299 | var res = {name: fixture.key, file: fixture, results: []};
|
300 | define(res, 'fixture', fixture);
|
301 | this.results.push(res);
|
302 |
|
303 | if (opts.dryRun === true) {
|
304 | files.forEach(function(file) {
|
305 | console.log(file.run(fixture.content));
|
306 | });
|
307 | return;
|
308 | }
|
309 |
|
310 | var suite = new Benchmark.Suite(fixture.bytes, {
|
311 | name: fixture.key,
|
312 | onStart: function onStart() {
|
313 | console.log(colors.cyan('\n# %s %s'), fixture.relative, fixture.bytes);
|
314 | },
|
315 | onComplete: function() {
|
316 | cursor.write('\n');
|
317 | }
|
318 | });
|
319 |
|
320 | files.forEach((file) => {
|
321 | suite.add(file.key, Object.assign({
|
322 | onCycle: function onCycle(event) {
|
323 | cursor.horizontalAbsolute();
|
324 | cursor.eraseLine();
|
325 | cursor.write(format(event.target));
|
326 | },
|
327 | fn: function() {
|
328 | return file.run.apply(null, utils.arrayify(fixture.content));
|
329 | },
|
330 | onComplete: function(event) {
|
331 | cursor.horizontalAbsolute();
|
332 | cursor.eraseLine();
|
333 | cursor.write(format(event.target));
|
334 | cursor.write('\n');
|
335 | res.results.push(utils.captureBench(event, file));
|
336 | },
|
337 | onAbort: this.emit.bind(this, 'error'),
|
338 | onError: this.emit.bind(this, 'error')
|
339 | }, options));
|
340 | });
|
341 |
|
342 | if (files.length <= 1) {
|
343 | this.emit('complete', res);
|
344 | return suite;
|
345 | }
|
346 |
|
347 | suite.on('complete', () => {
|
348 | var fastest = suite.filter('fastest').map('name');
|
349 | res.fastest = fastest;
|
350 | this.emit('complete', res);
|
351 | console.log(' fastest is', colors.green(fastest));
|
352 | });
|
353 |
|
354 | return suite;
|
355 | };
|
356 |
|
357 |
|
358 |
|
359 |
|
360 |
|
361 |
|
362 |
|
363 |
|
364 |
|
365 |
|
366 |
|
367 |
|
368 |
|
369 | Benchmarked.prototype.run = function(patterns, options, cb) {
|
370 | if (typeof options === 'function') {
|
371 | cb = options;
|
372 | options = { async: true };
|
373 | }
|
374 |
|
375 | var fixtures = this.filter('fixtures', patterns, options);
|
376 |
|
377 | if (fixtures.length > 0) {
|
378 | console.log('Benchmarking: (%d of %d)', fixtures.length, this.fixtures.files.length);
|
379 | fixtures.forEach(function(file) {
|
380 | console.log(' · %s', file.key);
|
381 | });
|
382 | } else {
|
383 | console.log('No matches for patterns: %s', util.inspect(patterns));
|
384 | }
|
385 |
|
386 | fixtures.forEach(function(file) {
|
387 | file.suite.run(options);
|
388 | });
|
389 |
|
390 | this.emit('end', this.results);
|
391 | };
|
392 |
|
393 | Benchmarked.prototype.dryRun = function(pattern, options, fn) {
|
394 | if (typeof options === 'function') {
|
395 | fn = options;
|
396 | options = null;
|
397 | }
|
398 |
|
399 | if (typeof pattern === 'function') {
|
400 | fn = pattern;
|
401 | options = {};
|
402 | pattern = '**/*';
|
403 | }
|
404 |
|
405 | if (typeof fn !== 'function') {
|
406 | throw new TypeError('Expected fn to be a function');
|
407 | }
|
408 |
|
409 | var opts = Object.assign({ async: true }, options);
|
410 | var fixtures = this.filter('fixtures', pattern, opts);
|
411 | var len = this.fixtures.files.length;
|
412 | var code = this.code;
|
413 |
|
414 | if (fixtures.length > 0) {
|
415 | console.log('Dry run for (%d of %d) fixtures:', fixtures.length, len);
|
416 | fixtures.forEach(function(file) {
|
417 | console.log(' · %s', file.key);
|
418 | });
|
419 | } else {
|
420 | console.log('No matches for pattern: %s', util.inspect(pattern));
|
421 | }
|
422 |
|
423 | console.log();
|
424 | code.files.forEach(function(file) {
|
425 | fixtures.forEach(function(fixture) {
|
426 | fn(file, fixture);
|
427 | });
|
428 | });
|
429 | };
|
430 |
|
431 | Benchmarked.run = function(options) {
|
432 | const opts = Object.assign({cwd: process.cwd()}, options);
|
433 |
|
434 | if (fs.existsSync(path.join(opts.cwd, 'benchmark'))) {
|
435 | opts.cwd = path.join(opts.cwd, 'benchmark');
|
436 | }
|
437 |
|
438 | return new Promise(function(resolve, reject) {
|
439 | const suite = new Benchmarked({
|
440 | cwd: __dirname,
|
441 | fixtures: path.join(opts.cwd, opts.fixtures),
|
442 | code: path.join(opts.cwd, opts.code)
|
443 | });
|
444 |
|
445 | suite.on('error', reject);
|
446 |
|
447 | if (opts.dry) {
|
448 | suite.dryRun(function(code, fixture) {
|
449 | console.log(colors.cyan('%s > %s'), code.key, fixture.key);
|
450 | const args = require(fixture.path).slice();
|
451 | const expected = args.pop();
|
452 | const actual = code.invoke.apply(null, args);
|
453 | console.log(expected, actual);
|
454 | console.log();
|
455 | resolve();
|
456 | });
|
457 | } else {
|
458 | suite.on('end', resolve);
|
459 | suite.run();
|
460 | }
|
461 | });
|
462 | };
|
463 |
|
464 | Benchmarked.render = function(benchmarks) {
|
465 | const res = [];
|
466 | for (let i = 0; i < benchmarks.length; i++) {
|
467 | const target = benchmarks[i];
|
468 | let b = `# ${target.name} ${target.file.bytes}\n`;
|
469 |
|
470 | for (let i = 0; i < target.results.length; i++) {
|
471 | const stats = target.results[i];
|
472 | b += ` ${stats.name} x ${stats.ops} ops/sec ±${stats.rme}%`;
|
473 | b += ` (${stats.runs} runs sampled)\n`;
|
474 | }
|
475 |
|
476 | b += `\n fastest is ${target.fastest.join(', ')}`;
|
477 | b += ` (by ${diff(target)} avg)\n`;
|
478 | res.push(b);
|
479 | }
|
480 | return res.join('\n');
|
481 | };
|
482 |
|
483 | function diff(target) {
|
484 | let len = target.results.length;
|
485 | let fastest = 0;
|
486 | let rest = 0;
|
487 |
|
488 | for (let i = 0; i < len; i++) {
|
489 | let stats = target.results[i];
|
490 |
|
491 | if (target.fastest.indexOf(stats.name) !== -1) {
|
492 | fastest = +stats.hz;
|
493 | } else {
|
494 | rest += +stats.hz;
|
495 | }
|
496 | }
|
497 | var avg = (fastest / (+rest / (len - 1)) * 100);
|
498 | return avg.toFixed() + '%';
|
499 | }
|
500 |
|
501 |
|
502 |
|
503 |
|
504 |
|
505 | module.exports = Benchmarked;
|