UNPKG

12 kBJavaScriptView Raw
1'use strict';
2
3/**
4 * Module dependencies
5 */
6
7const fs = require('fs');
8const path = require('path');
9const util = require('util');
10const File = require('vinyl');
11const ansi = require('ansi');
12const Emitter = require('component-emitter');
13const Benchmark = require('benchmark');
14const define = require('define-property');
15const reader = require('file-reader');
16const isGlob = require('is-glob');
17const typeOf = require('kind-of');
18const merge = require('mixin-deep');
19const glob = require('resolve-glob');
20const log = require('log-utils');
21const mm = require('micromatch');
22
23/**
24 * Local variables
25 */
26
27const utils = require('./lib/utils');
28const cursor = ansi(process.stdout);
29const colors = log.colors;
30
31/**
32 * Create an instance of Benchmarked with the given `options`.
33 *
34 * ```js
35 * const suite = new Suite();
36 * ```
37 * @param {Object} `options`
38 * @api public
39 */
40
41function 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 * Inherit Emitter
53 */
54
55util.inherits(Benchmarked, Emitter);
56
57/**
58 * Default settings
59 */
60
61Benchmarked.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 * Default formatter for benchmark.
93 *
94 * @param {Benchmark} `benchmark` The Benchmark to produce a string from.
95 */
96
97Benchmarked.prototype.format = function(benchmark) {
98 return ' ' + benchmark;
99};
100
101/**
102 * Create a vinyl file object.
103 *
104 * @param {String} `type` The type of file to create (`code` or `fixture`)
105 * @param {String} `filepath`
106 */
107
108Benchmarked.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 * Add fixtures to run benchmarks against.
127 *
128 * @param {String|Array} `patterns` Filepath(s) or glob patterns.
129 * @param {Options} `options`
130 */
131
132Benchmarked.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 * Add fixtures to run benchmarks against.
151 *
152 * @param {String|Array} `patterns` Filepath(s) or glob patterns.
153 * @param {Options} `options`
154 */
155
156Benchmarked.prototype.match = function(type, patterns, options) {
157 return mm.matchKeys(this[type].files, patterns, options);
158};
159
160/**
161 * Add fixtures to run benchmarks against.
162 *
163 * @param {String|Array} `patterns` Filepath(s) or glob patterns.
164 * @param {Options} `options`
165 */
166
167Benchmarked.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 * Add fixtures to run benchmarks against.
187 *
188 * @param {String|Array} `patterns` Filepath(s) or glob patterns.
189 * @param {Options} `options`
190 */
191
192Benchmarked.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 * Add an array of `files` to the files array and cache for the given `type`.
217 *
218 * @param {String} `type` Either `code` or `fixtures`
219 * @param {Array} Files to add
220 * @param {Object}
221 */
222
223Benchmarked.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 * Add a fixture to run benchmarks against.
232 *
233 * @param {String|Function} `fixture` Filepath or function
234 */
235
236Benchmarked.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 * Add a fixture to run benchmarks against.
245 *
246 * @param {String|Function} `fixture` Filepath or function
247 */
248
249Benchmarked.prototype.addFixture = function(file, options) {
250 this.addFile('fixtures', file, options);
251 return this;
252};
253
254/**
255 * Add fixtures to run benchmarks against.
256 *
257 * ```js
258 * benchmarks.addFixtures('fixtures/*.txt');
259 * ```
260 * @param {String|Array} `patterns` Filepaths or glob patterns.
261 * @param {Options} `options`
262 * @api public
263 */
264
265Benchmarked.prototype.addFixtures = function(files, options) {
266 this.addFiles('fixtures', files, options);
267 return this;
268};
269
270/**
271 * Specify the functions to be benchmarked.
272 *
273 * ```js
274 * benchmarks.addCode('fixtures/*.txt');
275 * ```
276 * @param {String|Array} `patterns` Filepath(s) or glob patterns.
277 * @param {Options} `options`
278 * @api public
279 */
280
281Benchmarked.prototype.addCode = function(file, options) {
282 this.addFiles('code', file, options);
283 return this;
284};
285
286/**
287 * Add benchmark suite to the given `fixture` file.
288 *
289 * @param {Object} `fixture` vinyl file object
290 * @api public
291 */
292
293Benchmarked.prototype.addSuite = function(fixture, options) {
294 var files = this.code.files;
295 var opts = this.options;
296 var format = this.format;
297
298 // capture results for this suite
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 * Run the benchmarks.
359 *
360 * ```js
361 * benchmarks.run();
362 * ```
363 * @param {Object} `options`
364 * @param {Function} `cb`
365 * @param {Object} `thisArg`
366 * @api public
367 */
368
369Benchmarked.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
393Benchmarked.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
431Benchmarked.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
464Benchmarked.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
483function 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 * Expose `Benchmarked`
503 */
504
505module.exports = Benchmarked;