├── .gitignore ├── .travis.yml ├── README.md ├── docs ├── CHANGELOG.md ├── docs.md └── todo.md ├── example ├── asyncs.js ├── basic.js ├── bench.js ├── configstore.js ├── math │ ├── async-first-faster.js │ ├── async-first-slower.js │ ├── sync-first-faster.js │ └── sync-first-slower.js ├── results.json └── single.js ├── package.json ├── src ├── BenchChain.js ├── Results.js ├── UI.js ├── battery.js ├── deps.js ├── index.js └── reports │ ├── GraphReporter.js │ ├── PercentReporter.js │ ├── Report.js │ └── TagReporter.js └── test ├── adding.js ├── chain.js ├── cli.js ├── index.js ├── persistance.js └── running.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Optional 2 | .nyc_output 3 | .fliphub 4 | coverage 5 | 6 | # Caches 7 | tmp 8 | _tmp 9 | .tmp 10 | .happypack 11 | 12 | # Dist/build output 13 | dist 14 | built 15 | build 16 | public 17 | 18 | 19 | # Numerous always-ignore extensions 20 | *.diff 21 | *.err 22 | *.orig 23 | *.log 24 | *.rej 25 | *.swo 26 | *.swp 27 | *.vi 28 | *~ 29 | *.sass-cache 30 | 31 | 32 | # OS or Editor folders 33 | .DS_Store 34 | .cache 35 | .project 36 | .settings 37 | .tmproj 38 | nbproject 39 | Thumbs.db 40 | .eslintcache 41 | .idea 42 | .vscode 43 | .vmconfig 44 | .tmConfig 45 | 46 | 47 | # NPM packages folder. 48 | node_modules/ 49 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | 4 | # https://docs.travis-ci.com/user/caching/#Clearing-Caches 5 | # cache: 6 | # apt: true 7 | # directories: 8 | # - node_modules 9 | 10 | # test with these versions 11 | node_js: 12 | - 7 13 | 14 | install: 15 | - npm install 16 | 17 | # @TODO: swap to yarn when needed 18 | # once installed, run these pkg scripts 19 | script: 20 | # - npm run build 21 | # - npm run lint 22 | # - npm run flow 23 | - npm test 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🏋️⛓ bench-chain 2 | 3 | > chainable benchmark recording - averages & graphs. 4 | 5 | [![Build Status][travis-image]][travis-url] 6 | [![NPM version][bench-chain-npm-image]][bench-chain-npm-url] 7 | [![MIT License][license-image]][license-url] 8 | [![bench-chain][gitter-badge]][gitter-url] 9 | [![Dependencies][david-deps-img]][david-deps-url] 10 | [![fluents][fluents-image]][fluents-url] 11 | 12 | [fluents-image]: https://img.shields.io/badge/⛓-fluent-9659F7.svg 13 | [fluents-url]: https://www.npmjs.com/package/flipchain 14 | 15 | [bench-chain-npm-image]: https://img.shields.io/npm/v/bench-chain.svg 16 | [bench-chain-npm-url]: https://npmjs.org/package/bench-chain 17 | [license-image]: http://img.shields.io/badge/license-mit-blue.svg?style=flat 18 | [license-url]: https://spdx.org/licenses/mit 19 | [gitter-badge]: https://img.shields.io/gitter/room/bench-chain/pink.svg 20 | [gitter-url]: https://gitter.im/bench-chain/Lobby 21 | 22 | [travis-image]: https://travis-ci.org/fluents/bench-chain.svg?branch=master 23 | [travis-url]: https://travis-ci.org/fluents/bench-chain 24 | 25 | [david-deps-img]: https://david-dm.org/bench-chain/bench-chain.svg 26 | [david-deps-url]: https://david-dm.org/bench-chain/bench-chain 27 | 28 | screen shot 2017-04-24 at 5 51 21 am 29 | 30 | 35 | 36 | extension of [benchmark.js](https://benchmarkjs.com/) 37 | 38 | ## 📦 install 39 | ```bash 40 | yarn add bench-chain 41 | npm i bench-chain --save 42 | ``` 43 | 44 | ```js 45 | const Bench = require('bench-chain') 46 | ``` 47 | 48 | ## [🌐 documentation](./docs) 49 | ## [🔬 tests](./tests) 50 | ## [📘 examples](./examples) 51 | 52 | ### 👋 basic 53 | ```js 54 | const Bench = require('bench-chain') 55 | 56 | Bench 57 | // location to store benchmarks 58 | .init(__dirname, 'basic.json') 59 | // tag current benchmarks with, to mark what changes caused differences 60 | .tags('v1') 61 | // actual benchmarks 62 | .add('1 * 1', () => 1 * 1) 63 | .add('1 + 1', () => 1 + 1) 64 | .run() 65 | ``` 66 | 67 | ### 💍 async 68 | ```js 69 | const Bench = require('bench-chain') 70 | 71 | const sleep = sleepDuration => new Promise(resolve => setTimeout(resolve, sleepDuration)) 72 | 73 | Bench 74 | .init().dir(__dirname).filename('asyncs.json').setup() 75 | .name('sleepy') 76 | .tags('v1,v2') 77 | 78 | // can also use .add, and then .runAsync() 79 | .addAsync('sleep1', async done => { 80 | await sleep(1000) 81 | done() 82 | }) 83 | .addAsync('sleep2', async done => { 84 | await sleep(2000) 85 | done() 86 | }) 87 | .run() 88 | ``` 89 | 90 | 91 | ### 🚩 flags 92 | 93 | [using funwithflags](https://github.com/aretecode/funwithflags) 94 | 95 | * `--graph` will show only the graph reporting, rather than run the benchmarks 96 | * `--run-times=10` will run the test `10` times 97 | 98 | 99 | ### 📇 metadata 100 | 101 |
102 | 🔋 battery parsing when available 103 | - will be used for comparing more benchmark results with averages 104 | - amperage (number) 105 | - currentCapacity (number) 106 | - percent (number) 107 | - charging (boolean) 108 | - temp (number) 109 |
110 | 111 | - **mem**: operating system memory, nodejs memory 112 | - **num**: operations a second from benchmarkjs hertz 113 | - **sampled**: total runs samples from benchmarkjs 114 | - **variation**: variation from benchmarkjs 115 | - **timesFor**: microtime | performance.now times for beginning & end of each run 116 | - **now**: Date.now for changes over time 117 | 118 | ### graphs for trends and variation 119 | 120 | screen shot 2017-04-29 at 9 08 50 pm 121 | 122 | 123 | ### grouped by tags 124 | 125 | screen shot 2017-05-01 at 5 45 33 am 126 | 127 | 128 | ### progress bars 129 | ![digress progress](https://cloud.githubusercontent.com/assets/4022631/25579989/ac2dc194-2e31-11e7-832b-75fa16b241e3.gif) 130 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.5.1 2 | - ☮️ compatibility: update fliptime with microtime polyfil that prefers microtime dep when it is available and accepts flags when available 3 | - ℹ️️ add cli --help flag 4 | - 🚩 add --reasoning as a flag to show math calculations for showing 5 | - 🏛️% refactor percentage reporting part 3 6 | - +- which is faster and slower, 7 | - %x whether to use percent or times, 8 | - <> whether to flip the values, 9 | - .. the digits on the numbers 10 | - 📘 add examples to display all 4 of those possibilities (since it compares in groups of 2, async and sync) 11 | - 🔬📝 test todos 12 | 13 | # 0.5.0 14 | - ⛓ put suite in store - breaking 15 | - 📦⬇ optional deps 16 | 17 | # 0.4.4 18 | - 📊 added current results to not use every single result which can max things out 19 | - ℹ️️ jsdocs for data adding with Results 20 | - 🛁 some configstore cleaning in Results 21 | - ⚒🔢 temp fix for slower/faster numbers 22 | - 🎀 prettier name for suiteName when using path, prettier objformat 23 | - 🖼️📦⬇ use mozilla polyfil for padEnd inline, less deps 24 | - 📈 added reporting of ops, slicing only most recent messages 25 | - 📜 script running examples 26 | - %📊 pct report improvements & 🛁 clean! 27 | - 📘 example of using for just a single benchmark 28 | 29 | # 0.4.3 30 | - 📛 fix tag reporting with referencing parent instead of reporter 31 | 32 | # 0.4.2 33 | - 🌀 fix ending spinner missed timeout alongside the interval 34 | - ⚙ fix json parsing in obj-chain-plugin-config 35 | 36 | # 0.4.1 37 | 38 | - 📒🚚 move memory helpers and other data into deps file 39 | - 📦⬆ added deps (table, some lodash, script-chain for auto-progress, obj-chain for config) 40 | - 🔬 adding more tests 41 | - ⚙💽 adding config store 42 | - 🖼️ add screenshots 43 | - 🏛️🏰 refactor reporting from Reporter, into 44 | - % PercentReporter 45 | - 📊 GraphReporter 46 | - # TagReporter 47 | - 🛁 take out avggraphsinone until it works 48 | - 🌀 add spinner, split into UI 49 | - 🤸 split BenchChain out of index into a file 50 | - 🆙📘 examples 51 | - 🔗 update docs links 52 | - 👂 add more subscribing methods 53 | - 👾 simplify some ops 54 | - ⛓ convert to more chainable store methods 55 | - 🚩 more cli options 56 | - ℹ️️ more jsdocs 57 | 58 | # 0.4.0 59 | - ⚒💍 fix async reporting some properties only being added to last cycle, just added to all now (bigger data but simpler code) 60 | - [...] auto-progress bars 61 | 62 | # 0.3.0 63 | - add name to suite 64 | - add more fluent 65 | - add docs for new stuff 66 | - added percent calc, likely needs improvements 67 | - more examples 68 | 69 | # 0.2.0 🔋⏱⛓📊👾 70 | - 🔋 battery parsing from plist when available 71 | - 🔋 battery in benchmark recordings 72 | - ⏱ microtime in recordings 73 | - ⛓ extend chainable 74 | - 📊 format microtime recordings 75 | - 👾 simplify async benchmarks 76 | - 🤸 split reports into another class 77 | - 🔬 fix test folder name and add another super simple basic instantiation test 78 | - 📈📉 more nice graphs 79 | -------------------------------------------------------------------------------- /docs/docs.md: -------------------------------------------------------------------------------- 1 | # 2 | 3 | 4 | 5 | ### src/index.js 6 | 7 | 8 | #### flow(funcs) 9 | 10 | 11 | 12 | 13 | 14 | 15 | ##### Parameters 16 | 17 | | Name | Type | Description | | 18 | | ---- | ---- | ----------- | -------- | 19 | | funcs | `Array.<Function>` | functions to flow left to right |   | 20 | 21 | 22 | 23 | 24 | ##### Returns 25 | 26 | 27 | - `Function` passes args through the functions, bound to this 28 | 29 | 30 | 31 | #### module.exports() 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | ##### Returns 41 | 42 | 43 | - `Void` 44 | 45 | 46 | 47 | #### formatNumber(number) 48 | 49 | Converts a number to a more readable comma-separated string representation. 50 | 51 | 52 | 53 | 54 | ##### Parameters 55 | 56 | | Name | Type | Description | | 57 | | ---- | ---- | ----------- | -------- | 58 | | number | `number` | The number to convert. |   | 59 | 60 | 61 | 62 | 63 | ##### Returns 64 | 65 | 66 | - `string` The more readable string representation. 67 | 68 | 69 | 70 | #### constructor(dir) 71 | 72 | 73 | 74 | 75 | 76 | 77 | ##### Parameters 78 | 79 | | Name | Type | Description | | 80 | | ---- | ---- | ----------- | -------- | 81 | | dir | `string` | directory for the file with the record |   | 82 | 83 | 84 | 85 | 86 | ##### Returns 87 | 88 | 89 | - `Void` 90 | 91 | 92 | 93 | #### cycle(event) *private method* 94 | 95 | 96 | 97 | 98 | 99 | 100 | ##### Parameters 101 | 102 | | Name | Type | Description | | 103 | | ---- | ---- | ----------- | -------- | 104 | | event | `Benchmark.Event` | |   | 105 | 106 | 107 | 108 | 109 | ##### Returns 110 | 111 | 112 | - `Record` @chainable 113 | 114 | 115 | 116 | #### filename() 117 | 118 | 119 | 120 | 121 | 122 | 123 | ##### Parameters 124 | 125 | | Name | Type | Description | | 126 | | ---- | ---- | ----------- | -------- | 127 | | filename='./results.json' | `String` | | *Optional* | 128 | 129 | 130 | 131 | 132 | ##### Returns 133 | 134 | 135 | - `Record` @chainable 136 | 137 | 138 | 139 | #### load([force=false]) 140 | 141 | 142 | 143 | 144 | 145 | 146 | ##### Parameters 147 | 148 | | Name | Type | Description | | 149 | | ---- | ---- | ----------- | -------- | 150 | | force=false | `Boolean` | force reload | *Optional* | 151 | 152 | 153 | 154 | 155 | ##### Returns 156 | 157 | 158 | - `Record` @chainable 159 | 160 | 161 | 162 | #### save() 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | ##### Returns 172 | 173 | 174 | - `Record` @chainable 175 | 176 | 177 | 178 | #### getDiv(max) 179 | 180 | 181 | 182 | 183 | 184 | 185 | ##### Parameters 186 | 187 | | Name | Type | Description | | 188 | | ---- | ---- | ----------- | -------- | 189 | | max | `number` | |   | 190 | 191 | 192 | 193 | 194 | ##### Returns 195 | 196 | 197 | - `number` 198 | 199 | 200 | 201 | #### trend() 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | ##### Returns 211 | 212 | 213 | - `Object.<points, max, min>` trend graph data 214 | 215 | 216 | 217 | #### avgs(prop) 218 | 219 | 220 | 221 | 222 | 223 | 224 | ##### Parameters 225 | 226 | | Name | Type | Description | | 227 | | ---- | ---- | ----------- | -------- | 228 | | prop | `string` | map to this property to average with that data |   | 229 | 230 | 231 | 232 | 233 | ##### Returns 234 | 235 | 236 | - `Averages` averages 237 | 238 | 239 | 240 | #### avg(data) 241 | 242 | 243 | 244 | 245 | 246 | 247 | ##### Parameters 248 | 249 | | Name | Type | Description | | 250 | | ---- | ---- | ----------- | -------- | 251 | | data | `Array.<number>` | |   | 252 | 253 | 254 | 255 | 256 | ##### Returns 257 | 258 | 259 | - `number` average 260 | 261 | 262 | 263 | #### fastest() 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | ##### Returns 273 | 274 | 275 | - `Array.<string>` test case name 276 | 277 | 278 | 279 | #### echoAvgs() 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | ##### Returns 289 | 290 | 291 | - `Record` @chainable 292 | 293 | 294 | 295 | #### echoFastest() 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | ##### Returns 305 | 306 | 307 | - `Record` @chainable 308 | 309 | 310 | 311 | #### echoTrend() 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | ##### Returns 321 | 322 | 323 | - `Record` @chainable 324 | 325 | 326 | 327 | #### suite(dir[, auto=false]) 328 | 329 | 330 | 331 | 332 | 333 | 334 | ##### Parameters 335 | 336 | | Name | Type | Description | | 337 | | ---- | ---- | ----------- | -------- | 338 | | dir | `string` | |   | 339 | | auto=false | `Boolean` | | *Optional* | 340 | 341 | 342 | 343 | 344 | ##### Returns 345 | 346 | 347 | - `Object` {suite, record} 348 | 349 | 350 | 351 | #### suite([auto=false]) 352 | 353 | 354 | 355 | 356 | 357 | 358 | ##### Parameters 359 | 360 | | Name | Type | Description | | 361 | | ---- | ---- | ----------- | -------- | 362 | | auto=false | `Boolean` | | *Optional* | 363 | 364 | 365 | 366 | 367 | ##### Returns 368 | 369 | 370 | - `Benchmark.Suite` 371 | 372 | 373 | 374 | #### setup([auto=true]) 375 | 376 | 377 | 378 | 379 | 380 | 381 | ##### Parameters 382 | 383 | | Name | Type | Description | | 384 | | ---- | ---- | ----------- | -------- | 385 | | auto=true | `Boolean` | automatically sets up echoing and saving | *Optional* | 386 | 387 | 388 | 389 | 390 | ##### Returns 391 | 392 | 393 | - `Record` @chainable 394 | 395 | 396 | 397 | #### add(name, cb) 398 | 399 | 400 | 401 | 402 | 403 | 404 | ##### Parameters 405 | 406 | | Name | Type | Description | | 407 | | ---- | ---- | ----------- | -------- | 408 | | name | `string` | |   | 409 | | cb | `Function` | |   | 410 | 411 | 412 | 413 | 414 | ##### Returns 415 | 416 | 417 | - `Record` @chainable 418 | 419 | 420 | 421 | #### run(async) 422 | 423 | 424 | 425 | 426 | 427 | 428 | ##### Parameters 429 | 430 | | Name | Type | Description | | 431 | | ---- | ---- | ----------- | -------- | 432 | | async | `boolean` | |   | 433 | 434 | 435 | 436 | 437 | ##### Returns 438 | 439 | 440 | - `Record` @chainable 441 | 442 | 443 | 444 | #### runAsync(async) 445 | 446 | 447 | 448 | 449 | 450 | 451 | ##### Parameters 452 | 453 | | Name | Type | Description | | 454 | | ---- | ---- | ----------- | -------- | 455 | | async | `boolean` | |   | 456 | 457 | 458 | 459 | 460 | ##### Returns 461 | 462 | 463 | - `Record` @chainable 464 | 465 | 466 | 467 | #### runTimes([times=10]) 468 | 469 | 470 | 471 | 472 | 473 | 474 | ##### Parameters 475 | 476 | | Name | Type | Description | | 477 | | ---- | ---- | ----------- | -------- | 478 | | times=10 | `Number` | | *Optional* | 479 | 480 | 481 | 482 | 483 | ##### Returns 484 | 485 | 486 | - `Record` @chainable 487 | 488 | 489 | 490 | 491 | *Documentation generated with [doxdox](https://github.com/neogeek/doxdox).* 492 | -------------------------------------------------------------------------------- /docs/todo.md: -------------------------------------------------------------------------------- 1 | ## 📝 todo 2 | https://gist.github.com/springmeyer/a67d8d77057d30fc4bbe 3 | https://mathiasbynens.be/notes/javascript-benchmarking 4 | https://github.com/JamieMason/karma-benchmark/blob/master/src/run-benchmarks.js 5 | https://github.com/avajs/ava/issues/816 6 | https://github.com/avajs/ava/blob/master/bench/compare.js 7 | https://benchmarkjs.com/docs#prototype_compare 8 | https://github.com/ngryman/speedracer 9 | https://esbench.com/bench/5908b65d99634800a0347e7b 10 | https://github.com/logicalparadox/matcha 11 | -------------------------------------------------------------------------------- /example/asyncs.js: -------------------------------------------------------------------------------- 1 | const Bench = require('../') 2 | 3 | const sleep = sleepDuration => 4 | new Promise(resolve => setTimeout(resolve, sleepDuration)) 5 | 6 | /* prettier-ignore */ 7 | 8 | Bench 9 | .init().dir(__dirname).filename('asyncs4.json').setup() 10 | .name('sleepy4') 11 | .tags('fresh,MORETAGS,uniqd,4th') 12 | 13 | // can also use .add, and then .runAsync() 14 | .addAsync('sleep1', async done => { 15 | await sleep(100) 16 | done() 17 | }) 18 | .addAsync('sleep2', async done => { 19 | await sleep(200) 20 | done() 21 | }) 22 | .addAsync('sleep3', async done => { 23 | await sleep(300) 24 | done() 25 | }) 26 | .addAsync('sleep200 4', async done => { 27 | await sleep(200) 28 | done() 29 | }) 30 | .run() 31 | -------------------------------------------------------------------------------- /example/basic.js: -------------------------------------------------------------------------------- 1 | const Bench = require('../src') 2 | 3 | Bench 4 | // location to store benchmarks 5 | .init(__dirname, 'basic.json') 6 | // tag current benchmarks with, to mark what changes caused differences 7 | .tags('v1') 8 | // actual benchmarks 9 | .add('1 * 1', () => 1 * 1) 10 | .add('1 + 1', () => 1 + 1) 11 | .run() 12 | 13 | // require('fliplog').quick(b) 14 | -------------------------------------------------------------------------------- /example/bench.js: -------------------------------------------------------------------------------- 1 | const {resolve} = require('path') 2 | const Bench = require('../') 3 | // const Bench = require('bench-chain') 4 | 5 | const {record, suite} = Bench.suite(__dirname, true) 6 | 7 | /* prettier-ignore */ 8 | 9 | suite 10 | .add('1 * 1', () => 1 * 1) 11 | .add('1 + 1', () => 1 + 1) 12 | .run() 13 | 14 | // true auto calls the following functions: 15 | record.setup(true) 16 | 17 | // suite.on('complete', () => { 18 | // record.echoFastest().save().echoAvgs().echoTrend() 19 | // }) 20 | -------------------------------------------------------------------------------- /example/configstore.js: -------------------------------------------------------------------------------- 1 | const Bench = require('../src') 2 | 3 | Bench 4 | // location to store benchmarks 5 | // .init(__dirname, 'configstore.basic.json') 6 | .init(__dirname, 'basic-configstore.json') 7 | .name('configstore-basic') 8 | // tag current benchmarks with, to mark what changes caused differences 9 | .tags('v0.4.1,configstore') 10 | // actual benchmarks 11 | .add('1 * 1', () => 1 * 1) 12 | .add('1 + 1', () => 1 + 1) 13 | .run() 14 | -------------------------------------------------------------------------------- /example/math/async-first-faster.js: -------------------------------------------------------------------------------- 1 | const Bench = require('../../') 2 | 3 | const sleep = sleepDuration => 4 | new Promise(resolve => setTimeout(resolve, sleepDuration)) 5 | 6 | /* prettier-ignore */ 7 | 8 | Bench 9 | .init().dir(__dirname).filename('async-first-faster.json').setup() 10 | .addAsync('zoomzoom', async done => { 11 | await sleep(10) 12 | done() 13 | }) 14 | .addAsync('snail', async done => { 15 | await sleep(200) 16 | done() 17 | }) 18 | .run() 19 | -------------------------------------------------------------------------------- /example/math/async-first-slower.js: -------------------------------------------------------------------------------- 1 | const Bench = require('../../') 2 | 3 | const sleep = sleepDuration => 4 | new Promise(resolve => setTimeout(resolve, sleepDuration)) 5 | 6 | /* prettier-ignore */ 7 | 8 | Bench 9 | .init().dir(__dirname).filename('async-first-faster2.json').setup() 10 | .addAsync('snail2', async done => { 11 | await sleep(200) 12 | done() 13 | }) 14 | .addAsync('zoomzoom2', async done => { 15 | await sleep(10) 16 | done() 17 | }) 18 | .run() 19 | -------------------------------------------------------------------------------- /example/math/sync-first-faster.js: -------------------------------------------------------------------------------- 1 | const Bench = require('../../src') 2 | const sleepfor = require('sleepfor') 3 | 4 | Bench 5 | .init(__dirname, 'sync-first-faster.json') 6 | .add('zoomzoom', () => sleepfor(100)) 7 | .add('snail', () => sleepfor(500)) 8 | .run() 9 | -------------------------------------------------------------------------------- /example/math/sync-first-slower.js: -------------------------------------------------------------------------------- 1 | const Bench = require('../../src') 2 | const sleepfor = require('sleepfor') 3 | 4 | Bench 5 | .init(__dirname, 'sync-first-slower.json') 6 | .add('snail2', () => sleepfor(500)) 7 | .add('zoomzoom2', () => sleepfor(100)) 8 | .run() 9 | -------------------------------------------------------------------------------- /example/results.json: -------------------------------------------------------------------------------- 1 | { 2 | "1 * 1": [ 3 | { 4 | "name": "1 * 1", 5 | "num": 79094295, 6 | "sampled": "73", 7 | "variation": "3.99", 8 | "now": 1493067493121, 9 | "mem": { 10 | "rss": 82219008, 11 | "heapTotal": 57655296, 12 | "heapUsed": 41964928, 13 | "external": 9284 14 | } 15 | }, 16 | { 17 | "name": "1 * 1", 18 | "num": 91186901, 19 | "sampled": "85", 20 | "variation": "1.59", 21 | "now": 1493067591310, 22 | "mem": { 23 | "rss": 85262336, 24 | "heapTotal": 61325312, 25 | "heapUsed": 43454624, 26 | "external": 9284 27 | } 28 | }, 29 | { 30 | "name": "1 * 1", 31 | "num": 52382049, 32 | "sampled": "63", 33 | "variation": "15.01", 34 | "now": 1493067724276, 35 | "mem": { 36 | "rss": 80969728, 37 | "heapTotal": 55558144, 38 | "heapUsed": 44062424, 39 | "external": 9284 40 | } 41 | }, 42 | { 43 | "name": "1 * 1", 44 | "num": 53357073, 45 | "sampled": "56", 46 | "variation": "15.19", 47 | "now": 1493067939578, 48 | "mem": { 49 | "rss": 79732736, 50 | "heapTotal": 55558144, 51 | "heapUsed": 43459944, 52 | "external": 9284 53 | } 54 | }, 55 | { 56 | "name": "1 * 1", 57 | "num": 74593115, 58 | "sampled": "80", 59 | "variation": "4.85", 60 | "now": 1493067969786, 61 | "mem": { 62 | "rss": 67575808, 63 | "heapTotal": 52400128, 64 | "heapUsed": 36618672, 65 | "external": 17476 66 | } 67 | }, 68 | { 69 | "name": "1 * 1", 70 | "num": 57964191, 71 | "sampled": "64", 72 | "variation": "13.83", 73 | "now": 1493068024073, 74 | "mem": { 75 | "rss": 68554752, 76 | "heapTotal": 52936704, 77 | "heapUsed": 37567840, 78 | "external": 17476 79 | } 80 | }, 81 | { 82 | "name": "1 * 1", 83 | "num": 58423933, 84 | "sampled": "70", 85 | "variation": "4.39", 86 | "now": 1493068091798, 87 | "mem": { 88 | "rss": 64589824, 89 | "heapTotal": 41402368, 90 | "heapUsed": 33064328, 91 | "external": 17476 92 | } 93 | }, 94 | { 95 | "name": "1 * 1", 96 | "num": 50831388, 97 | "sampled": "61", 98 | "variation": "15.94", 99 | "now": 1493068143464, 100 | "mem": { 101 | "rss": 66433024, 102 | "heapTotal": 51363840, 103 | "heapUsed": 33463776, 104 | "external": 17476 105 | } 106 | }, 107 | { 108 | "name": "1 * 1", 109 | "num": 70361357, 110 | "sampled": "77", 111 | "variation": "4.16", 112 | "now": 1493068220291, 113 | "mem": { 114 | "rss": 69046272, 115 | "heapTotal": 52936704, 116 | "heapUsed": 38118088, 117 | "external": 17476 118 | } 119 | }, 120 | { 121 | "name": "1 * 1", 122 | "num": 49500886, 123 | "sampled": "60", 124 | "variation": "12.07", 125 | "now": 1493068257622, 126 | "mem": { 127 | "rss": 67936256, 128 | "heapTotal": 51888128, 129 | "heapUsed": 36051824, 130 | "external": 17476 131 | } 132 | }, 133 | { 134 | "name": "1 * 1", 135 | "num": 79975791, 136 | "sampled": "81", 137 | "variation": "1.82", 138 | "now": 1493068314314, 139 | "mem": { 140 | "rss": 75505664, 141 | "heapTotal": 56082432, 142 | "heapUsed": 40194640, 143 | "external": 17476 144 | } 145 | }, 146 | { 147 | "name": "1 * 1", 148 | "num": 61770987, 149 | "sampled": "66", 150 | "variation": "13.07", 151 | "now": 1493068342530, 152 | "mem": { 153 | "rss": 67264512, 154 | "heapTotal": 51888128, 155 | "heapUsed": 35191864, 156 | "external": 17476 157 | } 158 | }, 159 | { 160 | "name": "1 * 1", 161 | "num": 73044801, 162 | "sampled": "77", 163 | "variation": "3.34", 164 | "now": 1493068354900, 165 | "mem": { 166 | "rss": 74080256, 167 | "heapTotal": 54497280, 168 | "heapUsed": 39144592, 169 | "external": 17476 170 | } 171 | }, 172 | { 173 | "name": "1 * 1", 174 | "num": 43940364, 175 | "sampled": "57", 176 | "variation": "10.19", 177 | "now": 1493068379182, 178 | "mem": { 179 | "rss": 66449408, 180 | "heapTotal": 51363840, 181 | "heapUsed": 34421864, 182 | "external": 17476 183 | } 184 | }, 185 | { 186 | "name": "1 * 1", 187 | "num": 61120489, 188 | "sampled": "67", 189 | "variation": "13.70", 190 | "now": 1493068417856, 191 | "mem": { 192 | "rss": 73781248, 193 | "heapTotal": 53985280, 194 | "heapUsed": 37096560, 195 | "external": 17476 196 | } 197 | }, 198 | { 199 | "name": "1 * 1", 200 | "num": 73289987, 201 | "sampled": "78", 202 | "variation": "3.69", 203 | "now": 1493068448748, 204 | "mem": { 205 | "rss": 75587584, 206 | "heapTotal": 56082432, 207 | "heapUsed": 37737376, 208 | "external": 17476 209 | } 210 | }, 211 | { 212 | "name": "1 * 1", 213 | "num": 52369948, 214 | "sampled": "65", 215 | "variation": "13.10", 216 | "now": 1493068507562, 217 | "mem": { 218 | "rss": 69550080, 219 | "heapTotal": 52936704, 220 | "heapUsed": 38969664, 221 | "external": 17476 222 | } 223 | }, 224 | { 225 | "name": "1 * 1", 226 | "num": 55871468, 227 | "sampled": "62", 228 | "variation": "13.29", 229 | "now": 1493068570470, 230 | "mem": { 231 | "rss": 69984256, 232 | "heapTotal": 53460992, 233 | "heapUsed": 39207864, 234 | "external": 17476 235 | } 236 | }, 237 | { 238 | "name": "1 * 1", 239 | "num": 78520221, 240 | "sampled": "82", 241 | "variation": "1.98", 242 | "now": 1493068591710, 243 | "mem": { 244 | "rss": 71430144, 245 | "heapTotal": 53985280, 246 | "heapUsed": 41143832, 247 | "external": 17476 248 | } 249 | }, 250 | { 251 | "name": "1 * 1", 252 | "num": 40949505, 253 | "sampled": "58", 254 | "variation": "7.12", 255 | "now": 1493068685252, 256 | "mem": { 257 | "rss": 64327680, 258 | "heapTotal": 40353792, 259 | "heapUsed": 32006976, 260 | "external": 17476 261 | } 262 | }, 263 | { 264 | "name": "1 * 1", 265 | "num": 50787392, 266 | "sampled": "61", 267 | "variation": "11.67", 268 | "now": 1493068767334, 269 | "mem": { 270 | "rss": 61997056, 271 | "heapTotal": 38244352, 272 | "heapUsed": 30969384, 273 | "external": 17476 274 | } 275 | }, 276 | { 277 | "name": "1 * 1", 278 | "num": 53756610, 279 | "sampled": "56", 280 | "variation": "14.22", 281 | "now": 1493068915439, 282 | "mem": { 283 | "rss": 64872448, 284 | "heapTotal": 41402368, 285 | "heapUsed": 33628392, 286 | "external": 17476 287 | } 288 | }, 289 | { 290 | "name": "1 * 1", 291 | "num": 53123132, 292 | "sampled": "56", 293 | "variation": "6.28", 294 | "now": 1493069003587, 295 | "mem": { 296 | "rss": 61743104, 297 | "heapTotal": 38244352, 298 | "heapUsed": 31829896, 299 | "external": 17476 300 | } 301 | }, 302 | { 303 | "name": "1 * 1", 304 | "num": 52404639, 305 | "sampled": "60", 306 | "variation": "15.13", 307 | "now": 1493069032016, 308 | "mem": { 309 | "rss": 65892352, 310 | "heapTotal": 50315264, 311 | "heapUsed": 31701368, 312 | "external": 17476 313 | } 314 | }, 315 | { 316 | "name": "1 * 1", 317 | "num": 53248751, 318 | "sampled": "59", 319 | "variation": "13.99", 320 | "now": 1493069059515, 321 | "mem": { 322 | "rss": 66351104, 323 | "heapTotal": 50315264, 324 | "heapUsed": 31947616, 325 | "external": 17476 326 | } 327 | }, 328 | { 329 | "name": "1 * 1", 330 | "num": 61700280, 331 | "sampled": "76", 332 | "variation": "7.58", 333 | "now": 1493069088390, 334 | "mem": { 335 | "rss": 71606272, 336 | "heapTotal": 53448704, 337 | "heapUsed": 40707656, 338 | "external": 17476 339 | } 340 | }, 341 | { 342 | "name": "1 * 1", 343 | "num": 65624344, 344 | "sampled": "79", 345 | "variation": "4.30", 346 | "now": 1493069112803, 347 | "mem": { 348 | "rss": 69386240, 349 | "heapTotal": 52936704, 350 | "heapUsed": 38642248, 351 | "external": 17476 352 | } 353 | }, 354 | { 355 | "name": "1 * 1", 356 | "num": 50967660, 357 | "sampled": "67", 358 | "variation": "7.83", 359 | "now": 1493069144235, 360 | "mem": { 361 | "rss": 69914624, 362 | "heapTotal": 52936704, 363 | "heapUsed": 38938656, 364 | "external": 17476 365 | } 366 | }, 367 | { 368 | "name": "1 * 1", 369 | "num": 42916885, 370 | "sampled": "55", 371 | "variation": "11.36", 372 | "now": 1493069171680, 373 | "mem": { 374 | "rss": 61714432, 375 | "heapTotal": 38256640, 376 | "heapUsed": 29467568, 377 | "external": 17476 378 | } 379 | }, 380 | { 381 | "name": "1 * 1", 382 | "num": 75251902, 383 | "sampled": "82", 384 | "variation": "1.64", 385 | "now": 1493069194338, 386 | "mem": { 387 | "rss": 76365824, 388 | "heapTotal": 57131008, 389 | "heapUsed": 41807544, 390 | "external": 17476 391 | } 392 | }, 393 | { 394 | "name": "1 * 1", 395 | "num": 48437372, 396 | "sampled": "59", 397 | "variation": "5.72", 398 | "now": 1493069227577, 399 | "mem": { 400 | "rss": 58163200, 401 | "heapTotal": 34574336, 402 | "heapUsed": 24153776, 403 | "external": 17476 404 | } 405 | }, 406 | { 407 | "name": "1 * 1", 408 | "num": 52728026, 409 | "sampled": "63", 410 | "variation": "13.66", 411 | "now": 1493069294498, 412 | "mem": { 413 | "rss": 66928640, 414 | "heapTotal": 51363840, 415 | "heapUsed": 34598360, 416 | "external": 17476 417 | } 418 | }, 419 | { 420 | "name": "1 * 1", 421 | "num": 34584743, 422 | "sampled": "65", 423 | "variation": "56.17", 424 | "now": 1493069352772, 425 | "mem": { 426 | "rss": 66973696, 427 | "heapTotal": 51888128, 428 | "heapUsed": 34662968, 429 | "external": 17476 430 | } 431 | } 432 | ], 433 | "1 + 1": [ 434 | { 435 | "name": "1 + 1", 436 | "num": 59165867, 437 | "sampled": "72", 438 | "variation": "14.62", 439 | "now": 1493067493121, 440 | "mem": { 441 | "rss": 82219008, 442 | "heapTotal": 57655296, 443 | "heapUsed": 41964928, 444 | "external": 9284 445 | } 446 | }, 447 | { 448 | "name": "1 + 1", 449 | "num": 91655924, 450 | "sampled": "87", 451 | "variation": "1.22", 452 | "now": 1493067591310, 453 | "mem": { 454 | "rss": 85262336, 455 | "heapTotal": 61325312, 456 | "heapUsed": 43454624, 457 | "external": 9284 458 | } 459 | }, 460 | { 461 | "name": "1 + 1", 462 | "num": 66144838, 463 | "sampled": "73", 464 | "variation": "4.00", 465 | "now": 1493067724276, 466 | "mem": { 467 | "rss": 80969728, 468 | "heapTotal": 55558144, 469 | "heapUsed": 44062424, 470 | "external": 9284 471 | } 472 | }, 473 | { 474 | "name": "1 + 1", 475 | "num": 59377437, 476 | "sampled": "73", 477 | "variation": "3.33", 478 | "now": 1493067939578, 479 | "mem": { 480 | "rss": 79732736, 481 | "heapTotal": 55558144, 482 | "heapUsed": 43459944, 483 | "external": 9284 484 | } 485 | }, 486 | { 487 | "name": "1 + 1", 488 | "num": 58642794, 489 | "sampled": "60", 490 | "variation": "8.36", 491 | "now": 1493067969786, 492 | "mem": { 493 | "rss": 67575808, 494 | "heapTotal": 52400128, 495 | "heapUsed": 36618672, 496 | "external": 17476 497 | } 498 | }, 499 | { 500 | "name": "1 + 1", 501 | "num": 57099483, 502 | "sampled": "74", 503 | "variation": "5.50", 504 | "now": 1493068024073, 505 | "mem": { 506 | "rss": 68554752, 507 | "heapTotal": 52936704, 508 | "heapUsed": 37567840, 509 | "external": 17476 510 | } 511 | }, 512 | { 513 | "name": "1 + 1", 514 | "num": 43356850, 515 | "sampled": "57", 516 | "variation": "15.75", 517 | "now": 1493068091798, 518 | "mem": { 519 | "rss": 64589824, 520 | "heapTotal": 41402368, 521 | "heapUsed": 33064328, 522 | "external": 17476 523 | } 524 | }, 525 | { 526 | "name": "1 + 1", 527 | "num": 49999569, 528 | "sampled": "77", 529 | "variation": "4.57", 530 | "now": 1493068143464, 531 | "mem": { 532 | "rss": 66433024, 533 | "heapTotal": 51363840, 534 | "heapUsed": 33463776, 535 | "external": 17476 536 | } 537 | }, 538 | { 539 | "name": "1 + 1", 540 | "num": 60424351, 541 | "sampled": "73", 542 | "variation": "3.41", 543 | "now": 1493068220291, 544 | "mem": { 545 | "rss": 69046272, 546 | "heapTotal": 52936704, 547 | "heapUsed": 38118088, 548 | "external": 17476 549 | } 550 | }, 551 | { 552 | "name": "1 + 1", 553 | "num": 66459294, 554 | "sampled": "78", 555 | "variation": "2.03", 556 | "now": 1493068257622, 557 | "mem": { 558 | "rss": 67936256, 559 | "heapTotal": 51888128, 560 | "heapUsed": 36051824, 561 | "external": 17476 562 | } 563 | }, 564 | { 565 | "name": "1 + 1", 566 | "num": 79748994, 567 | "sampled": "82", 568 | "variation": "2.72", 569 | "now": 1493068314314, 570 | "mem": { 571 | "rss": 75505664, 572 | "heapTotal": 56082432, 573 | "heapUsed": 40194640, 574 | "external": 17476 575 | } 576 | }, 577 | { 578 | "name": "1 + 1", 579 | "num": 63401871, 580 | "sampled": "76", 581 | "variation": "2.76", 582 | "now": 1493068342530, 583 | "mem": { 584 | "rss": 67264512, 585 | "heapTotal": 51888128, 586 | "heapUsed": 35191864, 587 | "external": 17476 588 | } 589 | }, 590 | { 591 | "name": "1 + 1", 592 | "num": 80030701, 593 | "sampled": "81", 594 | "variation": "1.55", 595 | "now": 1493068354900, 596 | "mem": { 597 | "rss": 74080256, 598 | "heapTotal": 54497280, 599 | "heapUsed": 39144592, 600 | "external": 17476 601 | } 602 | }, 603 | { 604 | "name": "1 + 1", 605 | "num": 76179932, 606 | "sampled": "79", 607 | "variation": "2.45", 608 | "now": 1493068379182, 609 | "mem": { 610 | "rss": 66449408, 611 | "heapTotal": 51363840, 612 | "heapUsed": 34421864, 613 | "external": 17476 614 | } 615 | }, 616 | { 617 | "name": "1 + 1", 618 | "num": 74538076, 619 | "sampled": "77", 620 | "variation": "2.93", 621 | "now": 1493068417856, 622 | "mem": { 623 | "rss": 73781248, 624 | "heapTotal": 53985280, 625 | "heapUsed": 37096560, 626 | "external": 17476 627 | } 628 | }, 629 | { 630 | "name": "1 + 1", 631 | "num": 76542551, 632 | "sampled": "82", 633 | "variation": "1.99", 634 | "now": 1493068448748, 635 | "mem": { 636 | "rss": 75587584, 637 | "heapTotal": 56082432, 638 | "heapUsed": 37737376, 639 | "external": 17476 640 | } 641 | }, 642 | { 643 | "name": "1 + 1", 644 | "num": 80617372, 645 | "sampled": "81", 646 | "variation": "2.16", 647 | "now": 1493068507562, 648 | "mem": { 649 | "rss": 69550080, 650 | "heapTotal": 52936704, 651 | "heapUsed": 38969664, 652 | "external": 17476 653 | } 654 | }, 655 | { 656 | "name": "1 + 1", 657 | "num": 80933812, 658 | "sampled": "82", 659 | "variation": "1.83", 660 | "now": 1493068570470, 661 | "mem": { 662 | "rss": 69984256, 663 | "heapTotal": 53460992, 664 | "heapUsed": 39207864, 665 | "external": 17476 666 | } 667 | }, 668 | { 669 | "name": "1 + 1", 670 | "num": 67793190, 671 | "sampled": "71", 672 | "variation": "4.09", 673 | "now": 1493068591710, 674 | "mem": { 675 | "rss": 71430144, 676 | "heapTotal": 53985280, 677 | "heapUsed": 41143832, 678 | "external": 17476 679 | } 680 | }, 681 | { 682 | "name": "1 + 1", 683 | "num": 59353877, 684 | "sampled": "71", 685 | "variation": "3.20", 686 | "now": 1493068685252, 687 | "mem": { 688 | "rss": 64327680, 689 | "heapTotal": 40353792, 690 | "heapUsed": 32006976, 691 | "external": 17476 692 | } 693 | }, 694 | { 695 | "name": "1 + 1", 696 | "num": 53019368, 697 | "sampled": "59", 698 | "variation": "8.39", 699 | "now": 1493068767334, 700 | "mem": { 701 | "rss": 61997056, 702 | "heapTotal": 38244352, 703 | "heapUsed": 30969384, 704 | "external": 17476 705 | } 706 | }, 707 | { 708 | "name": "1 + 1", 709 | "num": 58823364, 710 | "sampled": "70", 711 | "variation": "5.00", 712 | "now": 1493068915439, 713 | "mem": { 714 | "rss": 64872448, 715 | "heapTotal": 41402368, 716 | "heapUsed": 33628392, 717 | "external": 17476 718 | } 719 | }, 720 | { 721 | "name": "1 + 1", 722 | "num": 44496870, 723 | "sampled": "60", 724 | "variation": "12.00", 725 | "now": 1493069003587, 726 | "mem": { 727 | "rss": 61743104, 728 | "heapTotal": 38244352, 729 | "heapUsed": 31829896, 730 | "external": 17476 731 | } 732 | }, 733 | { 734 | "name": "1 + 1", 735 | "num": 49677405, 736 | "sampled": "74", 737 | "variation": "3.67", 738 | "now": 1493069032016, 739 | "mem": { 740 | "rss": 65892352, 741 | "heapTotal": 50315264, 742 | "heapUsed": 31701368, 743 | "external": 17476 744 | } 745 | }, 746 | { 747 | "name": "1 + 1", 748 | "num": 60804639, 749 | "sampled": "72", 750 | "variation": "3.66", 751 | "now": 1493069059515, 752 | "mem": { 753 | "rss": 66351104, 754 | "heapTotal": 50315264, 755 | "heapUsed": 31947616, 756 | "external": 17476 757 | } 758 | }, 759 | { 760 | "name": "1 + 1", 761 | "num": 73333313, 762 | "sampled": "80", 763 | "variation": "2.12", 764 | "now": 1493069088390, 765 | "mem": { 766 | "rss": 71606272, 767 | "heapTotal": 53448704, 768 | "heapUsed": 40707656, 769 | "external": 17476 770 | } 771 | }, 772 | { 773 | "name": "1 + 1", 774 | "num": 59740976, 775 | "sampled": "69", 776 | "variation": "13.41", 777 | "now": 1493069112803, 778 | "mem": { 779 | "rss": 69386240, 780 | "heapTotal": 52936704, 781 | "heapUsed": 38642248, 782 | "external": 17476 783 | } 784 | }, 785 | { 786 | "name": "1 + 1", 787 | "num": 73999858, 788 | "sampled": "80", 789 | "variation": "1.56", 790 | "now": 1493069144235, 791 | "mem": { 792 | "rss": 69914624, 793 | "heapTotal": 52936704, 794 | "heapUsed": 38938656, 795 | "external": 17476 796 | } 797 | }, 798 | { 799 | "name": "1 + 1", 800 | "num": 53260986, 801 | "sampled": "62", 802 | "variation": "11.20", 803 | "now": 1493069171680, 804 | "mem": { 805 | "rss": 61714432, 806 | "heapTotal": 38256640, 807 | "heapUsed": 29467568, 808 | "external": 17476 809 | } 810 | }, 811 | { 812 | "name": "1 + 1", 813 | "num": 74950906, 814 | "sampled": "82", 815 | "variation": "1.95", 816 | "now": 1493069194338, 817 | "mem": { 818 | "rss": 76365824, 819 | "heapTotal": 57131008, 820 | "heapUsed": 41807544, 821 | "external": 17476 822 | } 823 | }, 824 | { 825 | "name": "1 + 1", 826 | "num": 20145377, 827 | "sampled": "37", 828 | "variation": "14.99", 829 | "now": 1493069227577, 830 | "mem": { 831 | "rss": 58163200, 832 | "heapTotal": 34574336, 833 | "heapUsed": 24153776, 834 | "external": 17476 835 | } 836 | }, 837 | { 838 | "name": "1 + 1", 839 | "num": 62084055, 840 | "sampled": "71", 841 | "variation": "6.46", 842 | "now": 1493069294498, 843 | "mem": { 844 | "rss": 66928640, 845 | "heapTotal": 51363840, 846 | "heapUsed": 34598360, 847 | "external": 17476 848 | } 849 | }, 850 | { 851 | "name": "1 + 1", 852 | "num": 60876140, 853 | "sampled": "70", 854 | "variation": "3.56", 855 | "now": 1493069352772, 856 | "mem": { 857 | "rss": 66973696, 858 | "heapTotal": 51888128, 859 | "heapUsed": 34662968, 860 | "external": 17476 861 | } 862 | } 863 | ] 864 | } -------------------------------------------------------------------------------- /example/single.js: -------------------------------------------------------------------------------- 1 | const Bench = require('../') 2 | 3 | const sleep = sleepDuration => 4 | new Promise(resolve => setTimeout(resolve, sleepDuration)) 5 | 6 | /* prettier-ignore */ 7 | 8 | Bench 9 | .init(__dirname, 'single1') 10 | .addAsync('single1', async done => { 11 | await sleep(100) 12 | done() 13 | }) 14 | .run() 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.5.1", 3 | "name": "bench-chain", 4 | "description": "benchmark recording - averages & graphs.", 5 | "main": "src/index", 6 | "scripts": { 7 | "examples": "node example/asyncs && node example/basic && node example/configstore && node example/single", 8 | "test": "ava --verbose", 9 | "docs": "jsdoc" 10 | }, 11 | "todoDependencies": { 12 | "fluent-skeleton": "*" 13 | }, 14 | "optionalDependencies": { 15 | "microtime": "*" 16 | }, 17 | "dependencies": { 18 | "obj-chain-core": "0.0.7", 19 | "obj-chain-plugin-config": "0.0.7", 20 | "benchmark": "2.1.4", 21 | "cli-table2": "^0.2.0", 22 | "flipchain": "*", 23 | "flipfile": "*", 24 | "fliplog": "^0.2.7", 25 | "fliptime": "^3.0.2", 26 | "funwithflags": "*", 27 | "lodash.debounce": "^4.0.8", 28 | "lodash.flatten": "^4.4.0", 29 | "lodash.forown": "^4.4.0", 30 | "script-chain": "*" 31 | }, 32 | "devDependencies": { 33 | "ava": "*", 34 | "doxdox": "*", 35 | "fosho": "0.0.12", 36 | "jsdoc": "3.4.3", 37 | "jsdoc-api": "3.0.0", 38 | "jsdoc-babel": "^0.3.0", 39 | "tui-jsdoc-template": "^1.1.0", 40 | "fluent-skeleton": "*" 41 | }, 42 | "keywords": [ 43 | "benchmark", 44 | "ui", 45 | "average", 46 | "graph", 47 | "time", 48 | "record" 49 | ], 50 | "author": "James ", 51 | "license": "MIT", 52 | "bugs": { 53 | "url": "https://github.com/aretecode/bench-chain" 54 | }, 55 | "homepage": "https://github.com/aretecode/bench-chain", 56 | "repository": { 57 | "type": "git", 58 | "url": "git+https://github.com/aretecode/bench-chain.git" 59 | }, 60 | "jsdocs": { 61 | "source": { 62 | "include": [ 63 | "readme.md", 64 | "src" 65 | ], 66 | "includePattern": ".+\\.js(doc)?$" 67 | }, 68 | "opts": { 69 | "recurse": true, 70 | "destination": "./jsdocs", 71 | "template": "node_modules/tui-jsdoc-template", 72 | "package": "package.json" 73 | }, 74 | "plugins": [ 75 | "node_modules/jsdoc-babel", 76 | "plugins/markdown" 77 | ] 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/BenchChain.js: -------------------------------------------------------------------------------- 1 | /* eslint max-lines: "off" */ 2 | /* eslint import/no-dynamic-require: "off" */ 3 | 4 | const {Suite} = require('benchmark') 5 | const log = require('fliplog') 6 | const fliptime = require('fliptime') 7 | const Fun = require('funwithflags') 8 | const ChainedMap = require('flipchain/ChainedMapExtendable') 9 | const battery = require('./battery') 10 | const {getCurrentMemory, debounce} = require('./deps') 11 | const Interface = require('./UI') 12 | const Reporter = require('./reports/Report') 13 | const Results = require('./Results') 14 | 15 | const {microtime} = fliptime 16 | 17 | // cli arguments 18 | const argv = Fun(process.argv.slice(2), { 19 | default: { 20 | runTimes: 1, 21 | graph: false, 22 | dry: false, 23 | debug: false, 24 | noGraph: false, 25 | configStore: false, 26 | reasoning: false, 27 | help: false, 28 | }, 29 | bool: [ 30 | 'graph', 'debug', 31 | 'no-graph', 32 | 'silent', 33 | 'configStore', 34 | 'reasoning', 35 | 'help', 36 | ], 37 | alias: { 38 | noGraph: 'silent', 39 | configStore: ['file', 'config-store'], 40 | reasoning: ['calculations'], 41 | }, 42 | camel: true, 43 | unknown(arg, fun) { 44 | if (fun.i === 0) fun.argv.runTimes = Number(arg) 45 | }, 46 | }) 47 | let {runTimes, graph, dry, debug, noGraph, configStore, reasoning, help} = argv 48 | 49 | if (help) { 50 | const chalk = log.chalk() 51 | log 52 | .underline('bench-chain: --help') 53 | .fmtobj({ 54 | '--runTimes': { 55 | type: chalk.blue('Number'), 56 | default: 1, 57 | description: 'run the benchmarks multiple times', 58 | }, 59 | '--graph': { 60 | type: chalk.blue('Boolean'), 61 | default: false, 62 | description: 'only show the graph', 63 | }, 64 | '--noGraph': { 65 | type: chalk.blue('Boolean'), 66 | default: false, 67 | description: 'do not show the graph', 68 | }, 69 | '--dry': { 70 | type: chalk.blue('Boolean'), 71 | default: false, 72 | description: 'do not run the graph', 73 | }, 74 | '--debug': { 75 | type: chalk.blue('Boolean'), 76 | default: false, 77 | description: 'verbose debugging information', 78 | }, 79 | '--configStore': { 80 | type: chalk.blue('Boolean'), 81 | default: false, 82 | description: 'use configstore instead of the json file in source', 83 | }, 84 | '--reasoning': { 85 | type: chalk.blue('Boolean'), 86 | default: false, 87 | description: 'show math calculation reasoning for slower/faster', 88 | }, 89 | }) 90 | .echo() 91 | .exit() 92 | 93 | process.exit() 94 | } 95 | 96 | /** 97 | * @prop {string} store.dir directory 98 | * @prop {boolean} store.debug very verbose 99 | * @prop {Object} store.memory memory when started 100 | * @prop {number} store.testNames names of tests, useful for length 101 | * @prop {string} store.suiteName benchmarkjs suite name, defaults to filename 102 | * @prop {string} store.rel relative path to results json file 103 | * @prop {string} store.abs absolute path to results json file 104 | * @prop {Object} ui class for helping with spinners etc 105 | * @prop {Object} results class with json contents of file 106 | * @prop {Object} current current event target object 107 | * @prop {Array} timesFor microtime | performance.now times 108 | * 109 | * @TODO memoize subscriber cb check and normal for loop 110 | */ 111 | class BenchChain extends ChainedMap { 112 | constructor() { 113 | super() 114 | this.timesFor = {} 115 | this.tag = '' 116 | 117 | /* prettier-ignore */ 118 | 119 | this 120 | .extend([ 121 | 'dir', 122 | 'debug', 123 | 'testNames', 124 | 'memory', 125 | 'subscribers', 126 | 'configStore', 127 | 'reasoning', 128 | ]) 129 | .extendIncrement(['index']) 130 | .debug(debug) 131 | .reasoning(reasoning) 132 | .testNames([]) 133 | .memory(getCurrentMemory()) 134 | .subscribers({ 135 | cycle: [], 136 | complete: [], 137 | allComplete: [], 138 | }) 139 | .set('index', 0) 140 | 141 | this.echo = debounce(this.echo.bind(this), 2000) 142 | 143 | /* prettier-enable */ 144 | } 145 | 146 | /** 147 | * @param {string} [dir=null] directory for the file with the record 148 | * @param {string} [filename=null] filename for benchmark 149 | * @param {string} [debugOverride=false] debugOverride 150 | * @return {BenchChain} @chainable 151 | */ 152 | static init(dir = null, filename = null, debugOverride = false) { 153 | const bench = new BenchChain() 154 | 155 | if (debugOverride !== false) bench.debug(debugOverride) 156 | if (dir !== null) bench.dir(dir) 157 | if (filename !== null) bench.filename(filename) 158 | 159 | // for just use as a factory method 160 | if (!dir && !filename) return bench 161 | 162 | return bench.setup() 163 | } 164 | 165 | /** 166 | * @since 0.3.0 167 | * @param {string} name test name 168 | * @return {BenchChain} @chainable 169 | */ 170 | name(name) { 171 | return this.set('suiteName', name) 172 | } 173 | 174 | /** 175 | * @since 0.2.0 176 | * @param {string} tags tag current benchmarks with 177 | * @return {BenchChain} @chainable 178 | */ 179 | tags(tags) { 180 | return this.set('tags', tags) 181 | } 182 | 183 | // --- events --- 184 | 185 | /** 186 | * @event setup 187 | * @since 0.4.0 188 | * @param {Function} cb 189 | * @return {BenchChain} @chainable 190 | */ 191 | onSetup(cb) { 192 | this.get('suite').on('setup', cb) 193 | return this 194 | } 195 | 196 | /** 197 | * @event cycle 198 | * @since 0.4.0 199 | * @param {Function} cb 200 | * @return {BenchChain} @chainable 201 | */ 202 | onCycle(cb) { 203 | this.get('subscribers').cycle.push(cb) 204 | return this 205 | } 206 | 207 | /** 208 | * @event complete 209 | * @since 0.4.0 210 | * @param {Function} cb 211 | * @return {BenchChain} @chainable 212 | */ 213 | onComplete(cb) { 214 | this.get('subscribers').complete.push(cb) 215 | return this 216 | } 217 | 218 | /** 219 | * @event allComplete 220 | * @since 0.4.0 221 | * @param {Function} cb 222 | * @return {BenchChain} @chainable 223 | */ 224 | onAllComplete(cb) { 225 | this.get('subscribers').allComplete.push(cb) 226 | return this 227 | } 228 | 229 | /** 230 | * @event teardown 231 | * @since 0.4.0 232 | * @param {Function} cb 233 | * @return {BenchChain} @chainable 234 | */ 235 | onTeardown(cb) { 236 | this.get('suite').on('teardown', cb) 237 | return this 238 | } 239 | 240 | // --- helpers --- 241 | 242 | /** 243 | * @protected 244 | * @since 0.4.0 245 | * @see BenchChain.testName 246 | * @param {boolean} [latest=false] only use latest data 247 | * @return {Object} results, with test name when available 248 | */ 249 | getResults(latest = false) { 250 | return this.results.getForName(this.get('suiteName'), latest) 251 | } 252 | 253 | /** 254 | * @see BenchChain.suite 255 | * @desc filters benchmark results for fastest 256 | * @since 0.1.0 257 | * @return {Array} test case name 258 | */ 259 | fastest() { 260 | return this.get('suite').filter('fastest').map('name') 261 | } 262 | 263 | // --- file --- 264 | 265 | /** 266 | * @desc save and load file for the results 267 | * @since 0.2.0 268 | * @param {String} [filename='./results.json'] 269 | * @return {BenchChain} @chainable 270 | */ 271 | filename(filename = './results.json') { 272 | this.results = Results.init(this, configStore) 273 | .setup(this.get('dir'), filename) 274 | .load() 275 | 276 | return this.setup() 277 | } 278 | 279 | // --- subscribers --- 280 | 281 | /** 282 | * @protected 283 | * @since 0.2.0 284 | * @desc handles benchmark cycle event 285 | * @see BenchChain.results, BenchChain.current 286 | * @param {Benchmark.Event} event 287 | * @return {BenchChain} @chainable 288 | */ 289 | _onCycle(event) { 290 | const now = Date.now() 291 | const mem = { 292 | start: this.get('memory'), 293 | end: getCurrentMemory(), 294 | } 295 | 296 | const tags = this.get('tags') 297 | const suite = this.get('suiteName') 298 | const hz = event.target.hz < 100 ? 2 : 0 299 | const num = Number(event.target.hz.toFixed(hz)) 300 | 301 | // @example "optimized x 42,951 ops/sec ±3.45% (65 runs sampled)" 302 | const msg = event.target.toString() 303 | const sampled = msg.split('% (').pop().split(' runs').shift() 304 | const variation = msg.split('±').pop().split('%').shift() 305 | 306 | const {target} = event 307 | const {stats, count, cycles, errors, name} = target 308 | const timesFor = this.timesFor[name] 309 | 310 | const result = { 311 | msg, 312 | name, 313 | num, 314 | sampled, 315 | variation, 316 | tags, 317 | suite: [suite], 318 | timesFor, 319 | now, 320 | mem, 321 | stats, 322 | count, 323 | hz: target.hz, 324 | time: stats.mean * 1000, 325 | cycles, 326 | } 327 | 328 | // optimize 329 | if (battery) result.battery = battery 330 | if (errors) result.errors = errors 331 | 332 | this.current = result 333 | 334 | this.results.add(this.get('suiteName'), name, result) 335 | 336 | return this 337 | } 338 | 339 | /** 340 | * @protected 341 | * @desc after all benchmarks 342 | * @since 0.4.0 343 | * @return {BenchChain} @chainable 344 | */ 345 | _onAllCompleted() { 346 | const {subscribers, suiteName} = this.entries() 347 | subscribers.allComplete.forEach(cb => cb.call(this, this)) 348 | 349 | log.cyan('finished! ' + JSON.stringify(suiteName)).echo(this.get('debug')) 350 | 351 | this.ui.onAllComplete(suiteName) 352 | 353 | this.results.save() 354 | this.echo() 355 | 356 | return this 357 | } 358 | 359 | /** 360 | * @protected 361 | * @since 0.4.0 362 | * @NOTE complete is called at the end of *EACH* bench 363 | * @param {Benchmark.Event} event 364 | * @return {BenchChain} @chainable 365 | */ 366 | _onComplete(event) { 367 | const {testNames, index, subscribers} = this.entries() 368 | subscribers.complete.forEach(cb => cb.call(this, this)) 369 | 370 | const indexSaysDone = index === testNames.length 371 | const eventSaysDone = event.currentTarget.length === testNames.length 372 | 373 | if (indexSaysDone || eventSaysDone) this._onAllCompleted(event) 374 | else this.index() 375 | 376 | // log.dim('completed ' + testNames[index]).json(event).echo(this.get('debug')) 377 | log 378 | .dim('completed ' + testNames[index]) 379 | .data({ 380 | current: index, 381 | total: testNames.length, 382 | targetLen: event.currentTarget.length, 383 | }) 384 | .echo(this.get('debug')) 385 | 386 | return this 387 | } 388 | 389 | // --- suite --- 390 | 391 | /** 392 | * @see BenchChain.setup 393 | * @param {string} [override=null] defaults to this., or this.paths.abs 394 | * @return {Benchmark.Suite} 395 | */ 396 | suite(override = null) { 397 | const suiteName = 398 | override || this.get('suiteName') || this.results.get('abs') 399 | 400 | this.name(suiteName) 401 | 402 | this.set('suite', new Suite(suiteName)) 403 | 404 | return this.get('suite') 405 | } 406 | 407 | /** 408 | * @desc subscribes onCycle and onComplete 409 | * @since 0.1.0 410 | * @return {BenchChain} @chainable 411 | */ 412 | setup() { 413 | if (!this.has('suite')) this.suite() 414 | 415 | // setup ui 416 | this.ui = new Interface(this) 417 | 418 | // setup file 419 | // @TODO 420 | 421 | // setup name 422 | if (!this.get('suiteName')) { 423 | const rel = this.results 424 | .get('rel') 425 | .replace('json', '') 426 | .replace(/[./]/g, '') 427 | 428 | this.name(rel) 429 | } 430 | 431 | // bind the callbacks 432 | const cycle = this._onCycle.bind(this) 433 | const onComplete = this._onComplete.bind(this) 434 | 435 | // subscribe 436 | this.get('suite').on('cycle', event => cycle(event)) 437 | this.get('suite').on('complete', event => onComplete(event)) 438 | 439 | return this 440 | } 441 | 442 | // --- operations / bench helpers when not using suite / --- 443 | 444 | /** 445 | * @param {boolean} [asyncs=true] 446 | * @return {BenchChain} @chainable 447 | */ 448 | asyncMode(asyncs = true) { 449 | return this.set('asyncMode', asyncs) 450 | } 451 | 452 | /** 453 | * @protected 454 | * @since 0.4.0 455 | * @param {string} name 456 | * @return {BenchChain} @chainable 457 | */ 458 | addRecorder(name) { 459 | const results = this.getResults() 460 | const latest = this.getResults(true) 461 | 462 | // use results object, or a new object 463 | if (results !== undefined && results[name] === undefined) results[name] = [] 464 | else if (Array.isArray(results[name]) === false) results[name] = [] 465 | 466 | // same for latest 467 | if (latest !== undefined && latest[name] === undefined) latest[name] = [] 468 | else if (Array.isArray(latest[name]) === false) latest[name] = [] 469 | 470 | 471 | this.get('testNames').push(name) 472 | 473 | return this 474 | } 475 | 476 | /** 477 | * @protected 478 | * @since 0.2.0 479 | * @desc should return empty calls to see baseline 480 | * empty bench to get more raw overhead 481 | * 482 | * @see BenchChain.addAsync 483 | * @param {string} name test name 484 | * @param {Function} fn function to call deferred 485 | * @return {BenchChain} @chainable 486 | */ 487 | hijackAsync(name, fn) { 488 | return async cb => { 489 | if (!cb.reject) { 490 | cb.reject = e => { 491 | throw e 492 | } 493 | } 494 | 495 | const times = { 496 | start: null, 497 | end: null, 498 | } 499 | 500 | const hjResolve = arg => { 501 | times.end = microtime.now() 502 | times.diff = times.end - times.start 503 | return cb.resolve(arg) 504 | } 505 | const hjReject = arg => { 506 | times.end = microtime.now() 507 | times.diff = times.end - times.start 508 | delete times.end 509 | delete times.start 510 | 511 | return cb.reject(arg) 512 | } 513 | 514 | hjResolve.reject = hjReject 515 | hjResolve.resolve = hjResolve 516 | 517 | this.timesFor[name] = this.timesFor[name] || [] 518 | this.timesFor[name].push(times) 519 | 520 | // start timer after setup 521 | times.start = microtime.now() 522 | 523 | const called = await fn(hjResolve, hjReject) 524 | if (called && called.then) { 525 | called.then(arg => cb.resolve(arg)) 526 | } 527 | return called 528 | } 529 | } 530 | 531 | /** 532 | * @since 0.2.0 533 | * @desc add benchmark case (with defer) 534 | * @param {string} name 535 | * @param {Function} fn 536 | * @return {BenchChain} @chainable 537 | */ 538 | addAsync(name, fn) { 539 | this.set('asyncMode', true) 540 | this.get('suite').add(name, { 541 | defer: true, 542 | fn: this.hijackAsync(name, fn), 543 | }) 544 | 545 | return this.addRecorder(name) 546 | } 547 | 548 | /** 549 | * @desc add benchmark case 550 | * @since 0.1.0 551 | * @param {string} name 552 | * @param {Function} fn 553 | * @return {BenchChain} @chainable 554 | */ 555 | add(name, fn) { 556 | this.set('asyncMode', false) 557 | 558 | this.get('suite').add(name, fn) 559 | 560 | return this.addRecorder(name) 561 | } 562 | 563 | // --- ops --- 564 | 565 | /** 566 | * @since 0.1.0 567 | * @desc calls setup, runs suite 568 | * @return {BenchChain} @chainable 569 | */ 570 | run() { 571 | const {suiteName, asyncMode} = this.entries() 572 | 573 | if (dry) { 574 | log.warn('dry run').echo(this.get('debug')) 575 | return this 576 | } 577 | 578 | if (graph === true) { 579 | return this.echo() 580 | } 581 | 582 | log.cyan('starting! ' + JSON.stringify(suiteName)).echo(this.get('debug')) 583 | 584 | this.ui.onRun(suiteName) 585 | this.get('suite').run({async: asyncMode}) 586 | 587 | return this 588 | } 589 | 590 | /** 591 | * @TODO merge with .run, disable logs until end of all 592 | * @desc runs the suite test x times 593 | * @since 0.2.0 594 | * @param {Number} [times=runTimes] defaults to 1, allows first arg to be number of runs 595 | * @return {BenchChain} @chainable 596 | */ 597 | runTimes(times = runTimes) { 598 | if (times === null) times = runTimes 599 | 600 | const total = log.colored(times, 'bold') 601 | 602 | for (let i = 0; i < times; i++) { 603 | const current = log.colored(i, 'bold') 604 | const running = log.colored('running ', 'dim') 605 | const msg = `${running} ${current}/${total}` 606 | 607 | log.yellow('reset suite: ').data(msg).echo() 608 | 609 | this.get('suite').reset() 610 | this.get('suite').run({async: this.get('asyncMode')}) 611 | } 612 | 613 | return this 614 | } 615 | 616 | /** 617 | * @see this.filename 618 | * @NOTE debounced 619 | * @since 0.2.0 620 | * @desc instantiates Reporter, does echoing of numbers 621 | * @return {BenchChain} @chainable 622 | */ 623 | echo() { 624 | if (noGraph) return this 625 | 626 | const reporter = new Reporter(this) 627 | console.log('\n') 628 | reporter.echoFastest() 629 | reporter.echoAvgs() 630 | reporter.echoPercent() 631 | reporter.echoAvgGraph() 632 | reporter.echoTrend() 633 | reporter.echoOps() 634 | 635 | return this 636 | } 637 | } 638 | 639 | module.exports = BenchChain 640 | -------------------------------------------------------------------------------- /src/Results.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-dynamic-require: "off" */ 2 | const {resolve} = require('path') 3 | const ChainedMap = require('flipchain/ChainedMapExtendable') 4 | const exists = require('flipfile/exists') 5 | const write = require('flipfile/write') 6 | const log = require('fliplog') 7 | const ObjChain = require('obj-chain-core') 8 | 9 | /** 10 | * @TODO: should use obj-chain for the file as well so just a file swap, same api 11 | */ 12 | module.exports = class Results extends ChainedMap { 13 | 14 | /** 15 | * @param {BenchChain} parent 16 | * @param {boolean} [configStore=false] use configstore 17 | * @return {Results} 18 | */ 19 | static init(parent, configStore = false) { 20 | return new Results(parent, configStore) 21 | } 22 | 23 | /** 24 | * @param {BenchChain} parent 25 | * @param {boolean} [configStore=false] use configstore 26 | */ 27 | constructor(parent, configStore = false) { 28 | super(parent) 29 | 30 | if (configStore) { 31 | this.configStore(configStore) 32 | } 33 | 34 | if (parent && parent.has && parent.has('debug')) { 35 | this.debug(parent.get('debug')) 36 | } 37 | else { 38 | this.debug(false) 39 | } 40 | } 41 | 42 | /** 43 | * @desc use configstore via obj-chain (for easy escaping of `dot` syntax) 44 | * @since 0.4.4 45 | * @param {boolean} use 46 | * @return {BenchChain} @chainable 47 | */ 48 | configStore(use = true) { 49 | const configStore = new ObjChain({}, ['config']).setup().dot(false) 50 | return this.set('configStore', configStore) 51 | } 52 | 53 | /** 54 | * @since 0.5.0 55 | * @desc add data to record 56 | * @param {string} suiteName bench suite name 57 | * @param {string} name name of test 58 | * @param {Object} result data to record 59 | * @return {Results} @chainable 60 | */ 61 | add(suiteName, name, result) { 62 | const data1 = this.getForName(suiteName) 63 | const data2 = this.getForNameLatest(suiteName) 64 | 65 | if (!data1[name]) data1[name] = [] 66 | if (!data2[name]) data2[name] = [] 67 | data1[name].push(result) 68 | data2[name].push(result) 69 | 70 | return this 71 | } 72 | 73 | /** 74 | * @since 0.5.0 75 | * @desc latest run results only 76 | * @param {string} name suite name 77 | * @return {Object} results 78 | */ 79 | getForNameLatest(name) { 80 | if (name !== undefined) { 81 | if (this.latest[name] === undefined) { 82 | this.latest[name] = {} 83 | } 84 | return this.latest[name] 85 | } 86 | return this.latest 87 | } 88 | 89 | /** 90 | * @since 0.4.1 91 | * @desc gets results from file keyed 92 | * @param {string} name suite name 93 | * @param {boolean} [latest=false] latest run results only 94 | * @return {Object} results 95 | */ 96 | getForName(name, latest = false) { 97 | if (latest === true) { 98 | return this.getForNameLatest(name) 99 | } 100 | 101 | if (name !== undefined) { 102 | if (this.data[name] === undefined) { 103 | this.data[name] = {} 104 | } 105 | return this.data[name] 106 | } 107 | return this.data 108 | } 109 | 110 | /** 111 | * @desc resolve file, paths to file 112 | * sets abs: absolute path 113 | * sets rel: relative path 114 | * @since 0.4.1 115 | * @param {string} dir 116 | * @param {string} filename 117 | * @return {BenchChain} @chainable 118 | */ 119 | setup(dir, filename) { 120 | if (filename && !filename.includes('.json') && !filename.includes('.js')) { 121 | filename = filename + '.json' 122 | } 123 | const rel = filename || './results.json' 124 | const abs = resolve(dir, rel) 125 | return this.set('abs', abs).set('rel', rel) 126 | } 127 | 128 | /** 129 | * @protected 130 | * @desc load from file or configstore (still a file but diff) 131 | * @since 0.2.0 132 | * @see BenchChain.results 133 | * @param {boolean} [force=false] force reload 134 | * @return {BenchChain} @chainable 135 | */ 136 | load(force = false) { 137 | if (this.data && force === false) return this 138 | 139 | let {abs, configStore} = this.entries() 140 | this.latest = {} 141 | 142 | if (abs.includes('configstore') && !configStore) { 143 | configStore = this.configStore(true).get('configStore') 144 | 145 | log 146 | .green('results loaded from configstore: ') 147 | .json({'(cmd + click)': log.colored(configStore.path, 'underline')}) 148 | .echo() 149 | } 150 | 151 | // use configstore 152 | if (configStore) { 153 | log.underline('using configstore').echo(this.get('debug')) 154 | if (!configStore.has(abs)) { 155 | configStore.set(abs, {}) 156 | } 157 | this.data = configStore.get(abs) || {} 158 | 159 | return this 160 | } 161 | 162 | if (exists(abs) === false) write(abs, '{}') 163 | 164 | this.data = require(abs) 165 | log.green('loading').echo(this.get('debug')) 166 | 167 | return this 168 | } 169 | 170 | /** 171 | * @protected 172 | * @since 0.2.0 173 | * @desc saves to file or configstore 174 | * @see BenchChain.load, BenchChain.filename 175 | * @return {BenchChain} @chainable 176 | */ 177 | save() { 178 | log.green('saving').echo(this.get('debug')) 179 | const {configStore, abs} = this.entries() 180 | 181 | if (configStore) { 182 | configStore.set(abs, JSON.stringify(this.data)) 183 | 184 | log 185 | .green('results saved to: ') 186 | .json({'(cmd + click)': log.colored(configStore.path, 'underline')}) 187 | .echo() 188 | 189 | return this 190 | } 191 | 192 | write(abs, JSON.stringify(this.data)) 193 | 194 | return this 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/UI.js: -------------------------------------------------------------------------------- 1 | /* eslint lines-around-comment: "off" */ 2 | 3 | const log = require('fliplog') 4 | const ChainedMap = require('flipchain/ChainedMapExtendable') 5 | const {Remember} = require('script-chain') 6 | 7 | /** 8 | * @see configstore 9 | * @see script-chain 10 | * @prop {Remember} remember script to remember durations in configstore 11 | */ 12 | module.exports = class BenchChainUserInterface extends ChainedMap { 13 | constructor(parent) { 14 | super(parent) 15 | this.remember = new Remember() 16 | } 17 | 18 | /** 19 | * @event onAllComplete 20 | * @param {string} name 21 | * @return {BenchChainUserInterface} @chainable 22 | */ 23 | onRun(name) { 24 | this.remember.start(name, true) 25 | this.spinner() 26 | return this 27 | } 28 | 29 | /** 30 | * @event onAllComplete 31 | * @param {string} name 32 | * @return {BenchChainUserInterface} @chainable 33 | */ 34 | onAllComplete(name) { 35 | this.remember.finish(name, true) 36 | this.clearSpinners() 37 | return this 38 | } 39 | 40 | /** 41 | * @desc add a pseudo animated 42 | * @since 0.4.1 43 | * @return {BenchChain} @chainable 44 | */ 45 | spinner() { 46 | /** 47 | * @see this.spinning 48 | * @desc adds spinner for benchmarking 49 | * @type {Function} 50 | */ 51 | this.spin = () => { 52 | log.addSpinner('benchmarking', 'benchmarking') 53 | log.startSpinners([ 54 | ' 🏋️', 55 | ' 🏋️', 56 | ' 🏎', 57 | ' 🏎 ', 58 | ' 🏎 ∞', 59 | ' 🏎 ∞ ', 60 | ' 🏎 ∞∞ ', 61 | ' 🏎 ∞∞ ', 62 | ' 🏎 ∞ ', 63 | '🏎 ∞∞ ', 64 | ]) 65 | } 66 | this.spin() 67 | 68 | /** 69 | * @see this.spinning 70 | * @desc interval every 10 seconds swaps animated benchmarking for progress 71 | * @type {Function} 72 | */ 73 | this.removeSpinner = () => { 74 | try { 75 | log.removeSpinner() 76 | } 77 | catch (e) { 78 | // ignore 79 | } 80 | 81 | // reset and clear terminal 82 | log.clear() 83 | } 84 | 85 | /** 86 | * @see this.spinning 87 | * @desc interval every 10 seconds swaps animated benchmarking for progress 88 | * @type {Function} 89 | */ 90 | this.spinning = setInterval(() => { 91 | this.removeSpinner() 92 | this.spinAgain = setTimeout(() => this.spin(), 4000) 93 | }, 10000) 94 | 95 | /** 96 | * @see this.spinning 97 | * @desc function to clear interval and reset back 98 | * @type {Function} 99 | */ 100 | this.clearSpinners = () => { 101 | clearInterval(this.spinning) 102 | clearTimeout(this.spinAgain) 103 | this.removeSpinner() 104 | } 105 | 106 | return this 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/battery.js: -------------------------------------------------------------------------------- 1 | const {execFileSync} = require('child_process') 2 | 3 | const cmd = 'ioreg' 4 | const args = ['-n', 'AppleSmartBattery', '-r', '-a'] 5 | 6 | function strip(i) { 7 | return i.replace(/[\n\r\t]/gim, '').replace(/<\/?[^>]+(>|$)/g, '') 8 | } 9 | 10 | function plist(str) { 11 | const data = {} 12 | let current = {} 13 | str.split('\n').forEach(item => { 14 | if (item.includes('key') === true) { 15 | current.key = strip(item) 16 | } 17 | else { 18 | current.val = strip(item) 19 | data[current.key] = current.val 20 | current = {} 21 | } 22 | }) 23 | return data 24 | } 25 | 26 | function battery() { 27 | if (process.platform !== 'darwin') return 'NA: ' + process.platform 28 | return plist(execFileSync(cmd, args).toString()) 29 | } 30 | 31 | const b = battery() 32 | 33 | let Battery = {} 34 | if (process.platform === 'darwin') { 35 | Battery = { 36 | amperage: Number(b.Amperage), 37 | currentCapacity: Number(b.CurrentCapacity), 38 | percent: Math.floor(b.Current / b.Capacity * 100), 39 | charging: b.IsCharging, 40 | temp: Number(b.Temperature), 41 | } 42 | } 43 | 44 | module.exports = Battery 45 | -------------------------------------------------------------------------------- /src/deps.js: -------------------------------------------------------------------------------- 1 | const os = require('os') 2 | 3 | function uniq(value, index, arr) { 4 | return arr.indexOf(value) === index 5 | } 6 | 7 | /** 8 | * @param {Function[]} funcs functions to flow left to right 9 | * @return {Function} passes args through the functions, bound to this 10 | */ 11 | function flow(...funcs) { 12 | const length = funcs ? funcs.length : 0 13 | return function flowing(...args) { 14 | let index = 0 15 | // eslint-disable-next-line 16 | let result = length ? funcs[index].apply(this, args) : args[0] 17 | while (++index < length) { 18 | // eslint-disable-next-line 19 | result = funcs[index].call(this, result) 20 | } 21 | return result 22 | } 23 | } 24 | 25 | /** 26 | * Converts a number to a more readable comma-separated string representation. 27 | * 28 | * @static 29 | * @param {number} number The number to convert. 30 | * @return {string} The more readable string representation. 31 | */ 32 | function formatNumber(number) { 33 | number = String(number).split('.') 34 | return ( 35 | number[0].replace(/(?=(?:\d{3})+$)(?!\b)/g, ',') + 36 | (number[1] ? '.' + number[1] : '') 37 | ) 38 | } 39 | 40 | /** 41 | * @tutorial http://stackoverflow.com/questions/5799055/calculate-percentage-saved-between-two-numbers 42 | * @param {number} value 43 | * @param {number} other 44 | * @return {number} 45 | */ 46 | function calcTimes(value, other) { 47 | const diff = other / value 48 | 49 | // require('fliplog').quick({value, other, diff, fixed, end2, end3, end4, end5, end6, fixed2}) 50 | 51 | return diff 52 | } 53 | 54 | /** 55 | * @tutorial http://www.randomsnippets.com/2009/07/12/dynamic-or-on-the-fly-percentage-calculations-with-javascript/ 56 | * @param {number} oldval 57 | * @param {number} newval 58 | * @return {number} 59 | */ 60 | function calcPercent(oldval, newval) { 61 | var percentsavings = ((oldval - newval) / oldval) * 100 62 | return Math.round(percentsavings * 100) / 100 63 | } 64 | 65 | /** 66 | * @NOTE mutates obj 67 | * @param {Function} cb 68 | * @return {Function} to call with callback obj 69 | */ 70 | function flowVals(cb) { 71 | /** 72 | * @param {Object} obj 73 | * @return {Object} 74 | */ 75 | return function flowCb(obj) { 76 | const keys = Object.keys(obj) 77 | for (let i = 0; i < keys.length; i++) { 78 | const val = obj[keys[i]].map(str => str.length) 79 | obj[keys[i]] = cb(val) 80 | } 81 | return obj 82 | } 83 | } 84 | 85 | /** 86 | * @param {Array} data 87 | * @return {number} average 88 | */ 89 | function average(data) { 90 | const sum = data.reduce((prev, curr) => 0 + prev + curr, 0) 91 | return Math.floor(sum / data.length) 92 | } 93 | 94 | function standardDeviation(values) { 95 | const avg = average(values) 96 | const squareDiffs = values.map(value => { 97 | const diff = value - avg 98 | const sqrDiff = diff * diff 99 | return sqrDiff 100 | }) 101 | const avgSquareDiff = average(squareDiffs) 102 | const stdDev = Math.sqrt(avgSquareDiff) 103 | return stdDev 104 | } 105 | 106 | /** 107 | * @private 108 | * @desc divide by this number for nicer numbers 109 | * @param {number} max 110 | * @return {number} 111 | */ 112 | function getDiv(max) { 113 | switch (true) { 114 | case max > 1000: 115 | return 100 116 | case max > 10000: 117 | return 1000 118 | case max > 100000: 119 | return 10000 120 | case max > 1000000: 121 | return 100000 122 | case max > 10000000: 123 | return 1000000 124 | default: 125 | return 1 126 | } 127 | } 128 | 129 | // const flowmin = flow(Math.floor, Math.min) 130 | // const flowmax = flow(Math.floor, Math.max) 131 | const flowmin = nums => Math.floor(Math.min(...nums)) 132 | const flowmax = nums => Math.floor(Math.max(...nums)) 133 | // const flowmax = nums => { 134 | // if (!nums) return 0 135 | // return Math.floor(nums.reduce((a, b) => Math.max(a, b))) 136 | // } 137 | // const flowmin = nums => { 138 | // if (!nums) return 0 139 | // return Math.floor(nums.reduce((a, b) => Math.min(a, b))) 140 | // } 141 | // 142 | function getCurrentMemory(init = null) { 143 | return { 144 | process: process.memoryUsage(), 145 | os: os.freemem(), 146 | } 147 | } 148 | 149 | const _flatten = require('lodash.flatten') 150 | const flatten = arr => [].concat.apply(arr) 151 | const forown = require('lodash.forown') 152 | 153 | const mapown = (obj, cb) => { 154 | const mapped = [] 155 | forown(obj, (value, key, o) => { 156 | mapped.push(cb(value, key, o)) 157 | }) 158 | return mapped 159 | } 160 | 161 | const mapObjArr = (obj, cb) => { 162 | const mapped = [] 163 | forown(obj, (value, key, o) => { 164 | mapped.push(mapown(value, cb)) 165 | // mapped.push(cb(value, key, o)) 166 | }) 167 | return _flatten(mapped) 168 | } 169 | 170 | function groupBy(arr, property) { 171 | return arr.reduce((memo, x) => { 172 | if (!memo[x[property]]) { 173 | memo[x[property]] = [] 174 | } 175 | memo[x[property]].push(x) 176 | return memo 177 | }, {}) 178 | } 179 | 180 | // https://github.com/netcode/node-prettydate/blob/master/index.js 181 | function createHandler(divisor, noun, restOfString) { 182 | return function(diff) { 183 | var n = Math.floor(diff / divisor) 184 | var pluralizedNoun = noun + (n > 1 ? 's' : '') 185 | return '' + n + ' ' + pluralizedNoun + ' ' + restOfString 186 | } 187 | } 188 | 189 | var formatters = [ 190 | {threshold: -31535999, handler: createHandler(-31536000, 'year', 'from now')}, 191 | {threshold: -2591999, handler: createHandler(-2592000, 'month', 'from now')}, 192 | {threshold: -604799, handler: createHandler(-604800, 'week', 'from now')}, 193 | {threshold: -172799, handler: createHandler(-86400, 'day', 'from now')}, 194 | { 195 | threshold: -86399, 196 | handler() { 197 | return 'tomorrow' 198 | }, 199 | }, 200 | {threshold: -3599, handler: createHandler(-3600, 'hour', 'from now')}, 201 | {threshold: -59, handler: createHandler(-60, 'minute', 'from now')}, 202 | {threshold: -0.9999, handler: createHandler(-1, 'second', 'from now')}, 203 | { 204 | threshold: 1, 205 | handler() { 206 | return 'just now' 207 | }, 208 | }, 209 | {threshold: 60, handler: createHandler(1, 'second', 'ago')}, 210 | {threshold: 3600, handler: createHandler(60, 'minute', 'ago')}, 211 | {threshold: 86400, handler: createHandler(3600, 'hour', 'ago')}, 212 | { 213 | threshold: 172800, 214 | handler() { 215 | return 'yesterday' 216 | }, 217 | }, 218 | {threshold: 604800, handler: createHandler(86400, 'day', 'ago')}, 219 | {threshold: 2592000, handler: createHandler(604800, 'week', 'ago')}, 220 | {threshold: 31536000, handler: createHandler(2592000, 'month', 'ago')}, 221 | {threshold: Infinity, handler: createHandler(31536000, 'year', 'ago')}, 222 | ] 223 | 224 | function prettydate(date) { 225 | var diff = (new Date().getTime() - date.getTime()) / 1000 226 | for (var i = 0; i < formatters.length; i++) { 227 | if (diff < formatters[i].threshold) { 228 | return formatters[i].handler(diff) 229 | } 230 | } 231 | throw new Error('exhausted all formatter options, none found') // should never be reached 232 | } 233 | 234 | const debounce = require('lodash.debounce') 235 | 236 | // https://github.com/chalk/ansi-regex/blob/master/index.js 237 | const ansiRegex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nqry=><]/g 238 | const replaceAnsi = str => str.replace(ansiRegex, '') 239 | 240 | // https://github.com/uxitten/polyfill/blob/master/string.polyfill.js 241 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat 242 | if (!String.prototype.padEnd) { 243 | String.prototype.padEnd = function padEnd(targetLength, padString) { 244 | targetLength = targetLength >> 0 // floor if number or convert non-number to 0; 245 | padString = String(padString || ' ') 246 | if (this.length > targetLength) { 247 | return String(this) 248 | } 249 | else { 250 | targetLength = targetLength - this.length 251 | if (targetLength > padString.length) { 252 | padString += padString.repeat(targetLength / padString.length) // append to original to ensure we are longer than needed 253 | } 254 | return String(this) + padString.slice(0, targetLength) 255 | } 256 | } 257 | } 258 | 259 | module.exports = { 260 | replaceAnsi, 261 | uniq, 262 | flow, 263 | calcTimes, 264 | calcPercent, 265 | flowVals, 266 | average, 267 | getDiv, 268 | standardDeviation, 269 | flowmin, 270 | flowmax, 271 | getCurrentMemory, 272 | flatten, 273 | forown, 274 | mapown, 275 | mapObjArr, 276 | _flatten, 277 | groupBy, 278 | prettydate, 279 | debounce, 280 | } 281 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const log = require('fliplog') 2 | const fliptime = require('fliptime') 3 | const pkg = require('../package.json') 4 | const BenchChain = require('./BenchChain') 5 | 6 | log.registerCatch() 7 | 8 | // @TODO every time a bench is added, should register all here, for multi benches 9 | // const suites = {} 10 | 11 | BenchChain.Bench = BenchChain 12 | BenchChain.timer = fliptime 13 | BenchChain.log = log 14 | BenchChain.version = pkg.version 15 | module.exports = BenchChain 16 | -------------------------------------------------------------------------------- /src/reports/GraphReporter.js: -------------------------------------------------------------------------------- 1 | const log = require('fliplog') 2 | const {tillNow} = require('fliptime') 3 | const ChainedMap = require('flipchain/ChainedMapExtendable') 4 | let {average, getDiv, flowmin, flowmax, flatten} = require('../deps') 5 | 6 | module.exports = class GraphReporter { 7 | constructor(parent) { 8 | this.parent = parent 9 | this.debug = parent.debug 10 | this.loopResults = parent.loopResults.bind(parent) 11 | this.filterIfNeeded = parent.filterIfNeeded.bind(parent) 12 | this.avgs = parent.avgs.bind(parent) 13 | // this.trend = parent.trend.bind(parent) 14 | } 15 | 16 | /** 17 | * @TODO: abstract this 18 | * @see deps/getDiv 19 | * 20 | * @desc go through results, 21 | * get max and min, 22 | * pretty print numbers 23 | * 24 | * @return {Object} trend graph data 25 | */ 26 | trend() { 27 | const trend = {} 28 | 29 | this.loopResults((value, name) => { 30 | let timesFor 31 | let nums 32 | 33 | // skip for now 34 | if (!value || !value[0]) { 35 | log.yellow(name + ' had no value - yet').echo(this.debug) 36 | return 37 | } 38 | 39 | if (value[0].timesFor) { 40 | // remap 41 | timesFor = value 42 | .filter(v => v.timesFor) 43 | .map(entry => entry.timesFor.map(t => t.diff)) 44 | 45 | // flatten 46 | let nums1 = flatten(timesFor) 47 | 48 | // average 49 | nums = nums1.map(numnum => average(numnum)) 50 | } 51 | else { 52 | // log.quick('good') 53 | nums = value.map(entry => entry.num) 54 | } 55 | 56 | // min max 57 | let min = flowmin(nums) 58 | let max = flowmax(nums) 59 | const div = getDiv(max) 60 | if (this.max < max) this.max = max 61 | if (this.min < min) this.min = min 62 | 63 | // filter anomolies 64 | nums = this.filterIfNeeded({nums, min, max, div}) 65 | 66 | log.json({max, min, div}).text('trendy').echo(this.debug) 67 | 68 | max = max / div 69 | min = min / div 70 | 71 | // into graph points 72 | const points = nums 73 | .map((r, i) => { 74 | if (Math.floor(r / (div || 1)) === 0) return 0 75 | return [i, Math.floor(r / (div || 1))] 76 | }) 77 | .filter(r => r !== 0) 78 | 79 | // into graph points from date 80 | const datePoints = nums 81 | .map((r, i) => { 82 | let key = i 83 | 84 | const {ms, s, m, h, d, y} = tillNow(value[key].now) 85 | key = i 86 | 87 | if (m === 0) return 0 88 | return [key, m] 89 | }) 90 | .filter(r => r !== 0) 91 | 92 | trend[name] = {points, datePoints, max, min} 93 | }) 94 | 95 | // log.cyan('all trend data').verbose(100).data(trend).echo(this.debug) 96 | 97 | return trend 98 | } 99 | 100 | /** 101 | * @see Record.trend 102 | * @return {Record} @chainable 103 | */ 104 | echoTrend() { 105 | const graphs = this.trend() 106 | 107 | Object.keys(graphs).forEach(name => { 108 | console.log('\n') 109 | const {points, datePoints, max, min} = graphs[name] 110 | 111 | log 112 | .magenta('verbose graph:') 113 | .verbose(100) 114 | .data(graphs[name]) 115 | .echo(this.debug) 116 | 117 | log.data({points, datePoints, max, min}).echo(this.debug) 118 | 119 | log 120 | .barStyles({ 121 | color: 'green', 122 | width: 150, 123 | height: 10, 124 | maxY: max, 125 | yFractions: 0, 126 | caption: name, 127 | }) 128 | .bar(points) 129 | .echo(this.shouldEcho) 130 | 131 | log 132 | .barStyles({ 133 | color: 'yellow', 134 | width: 150, 135 | height: 10, 136 | yFractions: 0, 137 | caption: name + ' over time' + log.colored(' (minutes):', 'dim'), 138 | }) 139 | .bar(datePoints) 140 | .echo(false) 141 | // .echo(this.shouldEcho) 142 | }) 143 | 144 | return this 145 | } 146 | 147 | /** 148 | * @since 0.0.2 149 | * @see Record.avgs 150 | * @TODO transform data to trim 151 | * @return {Record} @chainable 152 | */ 153 | echoAvgGraph() { 154 | const avgs = this.avgs() 155 | const nums = Object.keys(avgs).map(name => Number(avgs[name])) 156 | const max = flowmax(nums) 157 | const min = flowmin(nums) 158 | const div = getDiv(max) // * 10 159 | 160 | if (this.max < max) this.max = max 161 | if (this.min < min) this.min = min 162 | 163 | // log.data({avgs, nums, max, min, div}).echo(thisdebug) 164 | 165 | const points = Object.keys(avgs).map((name, i) => { 166 | return [i, Math.floor(avgs[name] / div)] 167 | }) 168 | 169 | // , {max, min, nums, points} 170 | log.blue('averages of: ').data(Object.keys(avgs)).echo(this.debug) 171 | 172 | log 173 | .barStyles({ 174 | color: 'blue', 175 | maxY: Math.floor(max / div), 176 | minY: Math.floor(min / div), 177 | // width: 150, 178 | // height: 100, 179 | // yFractions: 0, 180 | // xFractions: 0, 181 | caption: 'averages of all:', 182 | }) 183 | .bar(points) 184 | .echo(false) 185 | // .echo(this.shouldEcho) 186 | 187 | return this 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/reports/PercentReporter.js: -------------------------------------------------------------------------------- 1 | // %%% %%%%%%%%%%%%%%% %%% 2 | // %%% percent calcs %%% 3 | // %%% %%%%%%%%%%%%%%% %%% 4 | 5 | const log = require('fliplog') 6 | const Table = require('cli-table2') 7 | let { 8 | padEnd, 9 | calcTimes, 10 | flowVals, 11 | calcPercent, 12 | flowmax, 13 | forown, 14 | replaceAnsi, 15 | } = require('../deps') 16 | 17 | /** 18 | * @see Reporter.avgs 19 | * @see Reporter.asyncMode 20 | */ 21 | module.exports = class PercentReporter { 22 | constructor(parent) { 23 | this.reasoning = parent.reasoning 24 | this.asyncMode = parent.asyncMode 25 | this.suiteName = parent.parent.get('suiteName') 26 | this.avgs = parent.avgs.bind(parent) 27 | } 28 | 29 | /** 30 | * @protected 31 | * @see [x] https://github.com/aretecode/bench-chain/issues/2 32 | * 33 | * @desc compares two numbers, 34 | * calculates times & percent, 35 | * adjusts wording based on results 36 | * calculate the positive and negatives * / x / times more/less 37 | * 38 | * @NOTE when in not-async mode, 39 | * it was using cycle ops instead of microtime diffs 40 | * but to simplify now both use the same 41 | * also because the hz in ops can be below 0 42 | * throwing off the calculations 43 | * 44 | * @since 0.4.1 45 | * @param {number} value 46 | * @param {number} other 47 | * @return {Object} {end, fixed, percent, word} 48 | */ 49 | calculatePercent(value, other) { 50 | const higherIsBetter = !this.asyncMode 51 | 52 | const data = { 53 | higherIsBetter, 54 | firstIsMore: value > other, 55 | negative: false, 56 | xy: calcTimes(value, other), 57 | yx: calcTimes(other, value), 58 | xyf: Math.floor(calcTimes(value, other)), 59 | yxf: Math.floor(calcTimes(other, value)), 60 | yxpercent: Math.floor(calcPercent(other, value)), 61 | xypercent: Math.floor(calcPercent(value, other)), 62 | } 63 | 64 | // default usually 65 | const shouldUseTimes = () => { 66 | return data.xyf !== 0 && data.yxf === 0 67 | } 68 | // if higher is better, if the calculates are revered, fix 69 | const shouldUseTimesFlipped = () => { 70 | return data.xyf === 0 && data.yxf !== 0 // && higherIsBetter 71 | } 72 | // usually used if below 0 && higher is not better 73 | const shouldUsePercent = () => { 74 | return data.xyf === 1 || data.yxf === 1 75 | } 76 | // check the calculations to return formatted string 77 | const format = () => { 78 | if (shouldUseTimes()) { 79 | return data.xyf + 'X' 80 | } 81 | if (shouldUseTimesFlipped()) { 82 | return data.yxf + 'X' 83 | } 84 | if (shouldUsePercent()) { 85 | return data.yxpercent + '%' 86 | } 87 | 88 | return data.yxf + 'X' 89 | } 90 | const slowerOrFaster = () => { 91 | // all nots 92 | if (!higherIsBetter && !data.firstIsMore && !data.negative) { 93 | return 'faster' 94 | } 95 | if (higherIsBetter && data.firstIsMore && !data.negative) { 96 | return 'faster' 97 | } 98 | 99 | return 'slower' 100 | } 101 | 102 | data.calculated = { 103 | msg: format() + ' ' + slowerOrFaster(), 104 | slowerOrFaster: slowerOrFaster(), 105 | shouldUseTimes: shouldUseTimes(), 106 | shouldUseTimesFlipped: shouldUseTimesFlipped(), 107 | shouldUsePercent: shouldUsePercent(), 108 | formatted: format(), 109 | } 110 | 111 | log.bold('======\n').fmtobj(data).echo(this.reasoning) 112 | 113 | const word = slowerOrFaster() 114 | const end = format() 115 | 116 | return {end, word} 117 | } 118 | 119 | /** 120 | * @TODO needs cleaning 121 | * 122 | * @since 0.3.0 123 | * @desc 124 | * uses microtime recordings & benchmarkjs data 125 | * to go through an average of averages 126 | * then compare each result to each other to show how many times 127 | * faster/slower they are 128 | * 129 | * @return {Record} @chainable 130 | */ 131 | echoPercent() { 132 | const avgs = this.avgs() 133 | const names = Object.keys(avgs) 134 | const values = Object.values(avgs) 135 | const pcts = [] 136 | const parts = { 137 | name: [], 138 | val: [], 139 | diff: [], 140 | word: [], 141 | compare: [], 142 | otherVal: [], 143 | } 144 | 145 | // add each part in so we know the lengths of each to padd 146 | const addPart = part => { 147 | parts.name.push(part.name) 148 | parts.val.push(part.val) 149 | parts.word.push(part.word) 150 | parts.diff.push(part.diff) 151 | parts.compare.push(part.compare) 152 | parts.otherVal.push(part.otherVal) 153 | pcts.push(part) 154 | } 155 | 156 | // go through each name 157 | // then go through each other name to compare 158 | names.forEach((name, n) => 159 | names.forEach((compare, i) => { 160 | if (compare === name) return 161 | 162 | const value = values[n] 163 | const other = avgs[compare] 164 | const {end, word} = this.calculatePercent(value, other) 165 | 166 | // format 167 | let vc = log.colored(value + '', 'green.underline') 168 | let oc = log.colored(other + '', 'green.underline') 169 | let ec = log.colored(end, 'bold') 170 | let wc = log.colored(word, 'italic') + ' than' 171 | let ns = [log.colored(name, 'cyan'), log.colored(compare, 'blue')] 172 | 173 | // wrap strings 174 | vc = `(${vc})` 175 | oc = `(${oc})` 176 | 177 | // put the parts into an array to format padding 178 | addPart({ 179 | name: ns[0], 180 | val: vc, 181 | diff: ec, 182 | word: wc, 183 | compare: ns[1], 184 | otherVal: oc, 185 | }) 186 | }) 187 | ) 188 | 189 | return this.echoPaddedAverages(pcts, names, parts).echoAvgTable(pcts, names) 190 | } 191 | 192 | /** 193 | * @protected 194 | * @since 0.4.1 195 | * @param {Array} pcts paddedColoredParts 196 | * @param {Array} names testnames 197 | * @param {Array} parts array of parts before padding and coloring 198 | * @return {Reporter} @chainable 199 | */ 200 | echoPaddedAverages(pcts, names, parts) { 201 | console.log('\n') 202 | let suiteName = this.suiteName 203 | 204 | if (suiteName.includes('/')) { 205 | suiteName = suiteName.split('/').pop() 206 | } 207 | if (suiteName.includes('.json')) { 208 | suiteName = suiteName.split('.json').shift() 209 | } 210 | 211 | log.bold(suiteName).echo() 212 | 213 | if (names[0]) { 214 | console.log('🏆 ' + log.colored(names[0].split(' ').pop(), 'underline')) 215 | } 216 | 217 | // padd end for pretty string 218 | const longests = flowVals(flowmax)(Object.assign({}, parts)) 219 | pcts.forEach(pct => { 220 | let str = '' 221 | forown(pct, (v, k) => { 222 | if (k === 'msg') return 223 | 224 | // pad first 225 | v = v.padEnd(longests[k] + 2) 226 | 227 | // because these emoji have different lengths in chars 228 | // but not terminal size so we replace here 229 | if (v.includes('faster')) { 230 | v = v.replace(/(faster)/g, '🏎️') // 🏎️ ⚡ 231 | } 232 | else if (v.includes('slower')) { 233 | v = v.replace(/(slower)/g, '🐌') 234 | } 235 | v = v.replace(/(than)/g, log.colored(' than', 'dim')) 236 | 237 | str += v 238 | }) 239 | console.log(str) 240 | }) 241 | console.log('\n') 242 | 243 | return this 244 | } 245 | 246 | /** 247 | * @protected 248 | * @since 0.4.1 249 | * @param {Array} pcts paddedColoredParts 250 | * @param {Array} names testnames 251 | * @return {Reporter} @chainable 252 | */ 253 | echoAvgTable(pcts, names) { 254 | const avgLong = this.avgs() 255 | const table = new Table({head: pcts.map(p => p.name)}) 256 | 257 | const rows = pcts.map((p, i) => { 258 | const strippedName = replaceAnsi(p.name) 259 | if (!avgLong[strippedName]) { 260 | log.red('could not average it out' + p.name).echo() 261 | // log.quick({avgLong, names, strippedName, i, p}) 262 | return '' 263 | } 264 | 265 | return avgLong[strippedName] 266 | }) 267 | 268 | table.push(rows) 269 | console.log(table.toString()) 270 | return this 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/reports/Report.js: -------------------------------------------------------------------------------- 1 | const log = require('fliplog') 2 | const ChainedMap = require('flipchain/ChainedMapExtendable') 3 | let {average, flowmin, flowmax, flatten, mapown, mapObjArr} = require('../deps') 4 | const TagReporter = require('./TagReporter') 5 | const PercentReporter = require('./PercentReporter') 6 | const GraphReporter = require('./GraphReporter') 7 | 8 | /** 9 | * @TODO 10 | * - [ ] deepmerge multi results 11 | * - [ ] graph comparisons 12 | * @type {Class} 13 | */ 14 | module.exports = class Report extends ChainedMap { 15 | constructor(parent) { 16 | super(parent) 17 | 18 | const {debug, asyncMode, reasoning} = parent.entries() 19 | 20 | this.fastest = parent.fastest.bind(parent) 21 | this.getResults = (latest = false) => this.parent.getResults(latest) 22 | this.getNames = () => this.parent.get('testNames') 23 | this.getResultsWithNames = () => { 24 | return {names: this.getNames(), results: this.getResults()} 25 | } 26 | 27 | // @TODO improve 28 | this.reasoning = reasoning 29 | this.shouldEcho = true 30 | this.shouldFilter = false 31 | this.asyncMode = asyncMode 32 | this.debug = debug 33 | this.max = 0 34 | this.min = 0 35 | this.mean = 0 36 | 37 | this.tagReporter = new TagReporter(this) 38 | this.percentReporter = new PercentReporter(this) 39 | this.graphReporter = new GraphReporter(this) 40 | 41 | this.echoAvgGraph = this.graphReporter.echoAvgGraph.bind(this.graphReporter) 42 | this.echoTrend = this.graphReporter.echoTrend.bind(this.graphReporter) 43 | this.echoPercent = () => { 44 | this.percentReporter.echoPercent() 45 | this.tagReporter.echoTagged() 46 | } 47 | } 48 | 49 | // --- transformers (fbp here makes sense) --- 50 | 51 | /** 52 | * @since 0.2.0 53 | * @see BenchChain.asyncMode 54 | * @param {string} prop map to this property to average with that data 55 | * @return {Averages} averages 56 | */ 57 | avgs(prop = 'num') { 58 | const avgs = {} 59 | const {results, names} = this.getResultsWithNames() 60 | 61 | log.blue('this.results').data({results, names}).echo(this.debug) 62 | 63 | // results(keys[0]).timesFor 64 | if (this.asyncMode) { 65 | names.forEach(name => { 66 | const value = results[name] 67 | 68 | // skip for now 69 | if (!value || !value[0] || !value[0].timesFor) { 70 | log.yellow(name + ' had no value - yet').echo(this.debug) 71 | return 72 | } 73 | 74 | // gather 75 | const timesForMulti = value 76 | .map(entry => { 77 | log 78 | .data(entry, entry.timesFor) 79 | .red('ENTRY ' + entry.name) 80 | .echo(this.debug) 81 | 82 | if (entry.timesFor === undefined) { 83 | // log.quick(entry) 84 | return false 85 | } 86 | return entry.timesFor.map(t => t.diff) 87 | }) 88 | .filter(val => val) 89 | 90 | // flatten 91 | const timesFor = flatten(timesForMulti).pop() 92 | 93 | const max = flowmax(timesFor) 94 | const min = flowmax(timesFor) 95 | if (this.max < max) this.max = max 96 | if (this.min < min) this.min = min 97 | 98 | const avgavg = Math.abs(average(timesFor)) 99 | 100 | // more averages 101 | const resultsForProp = value.map(result => avgavg) 102 | const avg = average(resultsForProp) 103 | 104 | log 105 | .blue('averages') 106 | .data({name, resultsForProp, avg, avgavg, timesFor}) 107 | .echo(this.debug) 108 | 109 | avgs[name] = avg 110 | }) 111 | } 112 | else { 113 | return this.propAvg() 114 | } 115 | 116 | return avgs 117 | } 118 | 119 | /** 120 | * @desc same as avg, but using a specific prop 121 | * @since 0.4.1 122 | * @param {String} [prop='num'] 123 | * @return {Array} 124 | */ 125 | propAvg(prop = 'num') { 126 | const avgs = {} 127 | const {results, names} = this.getResultsWithNames() 128 | 129 | names.forEach(name => { 130 | const resultsForProp = results[name].map(result => Number(result[prop])) 131 | const avg = average(resultsForProp) 132 | log.blue('averages').data({name, resultsForProp, avg}).echo(this.debug) 133 | avgs[name] = avg 134 | }) 135 | 136 | return avgs 137 | } 138 | 139 | // --- echoing helpers 2 --- 140 | 141 | /** 142 | * @since 0.4.1 143 | * @param {Function} cb 144 | * @return {Array} mapped results 145 | */ 146 | loopResults(cb) { 147 | return mapown(this.getResults(), cb) 148 | } 149 | 150 | /** 151 | * @desc filters numbers outside of the usual range if needed 152 | * @param {number} nums 153 | * @param {number} min 154 | * @param {number} max 155 | * @return {Array} 156 | */ 157 | filterIfNeeded({nums, min, max}) { 158 | if (!this.shouldFilter) return nums 159 | 160 | nums = nums.filter(nn => { 161 | const minp = min * 1.1 162 | const maxp = max / 1.1 163 | 164 | log 165 | .data({ 166 | nn, 167 | minp, 168 | maxp, 169 | max, 170 | min, 171 | passes: nn >= minp && nn <= maxp, 172 | }) 173 | .echo(false) 174 | 175 | return nn >= minp // && (nn <= maxp) 176 | }) 177 | 178 | return nums 179 | } 180 | 181 | /** 182 | * @param {number} avg 183 | * @return {string} color 184 | */ 185 | colorForAvg(avg) { 186 | const avgMin = avg > this.min * 1.1 187 | const minmin = avg > this.min * 1.1 188 | const minish = avg > this.min * 1.5 189 | const abvAvg = avg > this.max / 1.3 190 | const avgish = avg > this.max / 1.1 191 | const wow = avg > this.max / 1.05 192 | if (wow) return 'bold' 193 | if (avgish) return 'green' 194 | if (abvAvg || minish) return 'dim' 195 | if (avgMin) return 'yellow' 196 | if (minmin) return 'red' 197 | return 'red' 198 | } 199 | 200 | // --- simple echos --- 201 | 202 | /** 203 | * @see Record.avgs 204 | * @TODO transform data to trim 205 | * @return {Record} @chainable 206 | */ 207 | echoAvgs() { 208 | // in async mode, uses microtime diffs as ops are not as reliable 209 | let msg = 'lower is better, time taken in microseconds' 210 | 211 | // when in sync mode, it is ops/second instead 212 | if (!this.asyncMode) { 213 | msg = 'higher is better, operations per second' 214 | } 215 | 216 | log 217 | .color('dim.italic') 218 | .text(msg) 219 | .echo() 220 | 221 | log 222 | .fmtobj(this.avgs()) 223 | .bold('averages:') 224 | .echo(this.shouldEcho) 225 | 226 | return this 227 | } 228 | 229 | /** 230 | * @see Record.fastest 231 | * @return {Record} @chainable 232 | */ 233 | echoFastest() { 234 | log 235 | .verbose(this.fastest().shift()) 236 | .underline('Fastest is ') 237 | .echo(this.shouldEcho) 238 | 239 | return this 240 | } 241 | 242 | /** 243 | * @since 0.4.1 244 | * @desc very long message echoing 245 | * for all cycles of all test echoing, 246 | * should filter 247 | * @return {Record} @chainable 248 | */ 249 | echoOps() { 250 | this.getResults() 251 | const {results, names} = this.getResultsWithNames() 252 | const msgs = [] 253 | 254 | msgs.push('-----') 255 | names.forEach(name => { 256 | const value = results[name].slice(0).reverse() 257 | let limit = 10 258 | for (let i = 0; i < limit && i < value.length; i++) { 259 | if (!value[i].msg) { 260 | limit++ 261 | continue 262 | } 263 | msgs.push(value[i].msg) 264 | } 265 | 266 | msgs.push('-----') 267 | }) 268 | 269 | log.bold('\n\noperations per second\n').echo() 270 | msgs.forEach(msg => console.log(msg)) 271 | 272 | // works too, but harder to filter 273 | // const msgs = mapObjArr(this.getResults(), data => data.msg).reverse() 274 | 275 | return this 276 | } 277 | // --- -------------- --- 278 | // --- graph building --- 279 | // --- -------------- --- 280 | } 281 | -------------------------------------------------------------------------------- /src/reports/TagReporter.js: -------------------------------------------------------------------------------- 1 | // ### ############ 2 | // ### tags ### 3 | // ### ############ 4 | 5 | const log = require('fliplog') 6 | const Table = require('cli-table2') 7 | let {forown, mapown, groupBy, prettydate} = require('../deps') 8 | 9 | /** 10 | * @see Reporter.getResultsWithNames 11 | * @see Reporter.avgs 12 | * @see Reporter.colorForAvg 13 | * @see Reporter.debug 14 | * @see Reporter.loopResults 15 | */ 16 | module.exports = class TableReporter { 17 | constructor(parent) { 18 | this.parent = parent 19 | this.debug = parent.debug 20 | this.getResultsWithNames = parent.getResultsWithNames.bind(parent) 21 | this.loopResults = parent.loopResults.bind(parent) 22 | this.avgs = parent.avgs.bind(parent) 23 | this.colorForAvg = parent.colorForAvg.bind(parent) 24 | } 25 | 26 | /** 27 | * @see this._regroupTags 28 | * @see this._makeTagTableRows 29 | * @desc makes a table, regroups tags, puts tags into a table 30 | * @since 0.4.1 31 | * @return {Reports} @chainable 32 | */ 33 | echoTagged() { 34 | const table = new Table({ 35 | hAlign: 'center', 36 | head: ['name', 'tags', 'averages', 'time'], 37 | }) 38 | 39 | const regrouped = this._regroupTags() 40 | const tds = this._makeTagTableRows(regrouped) 41 | 42 | table.push(...tds) 43 | console.log(table.toString()) 44 | 45 | return this 46 | } 47 | 48 | /** 49 | * @desc regroup results by tag with pretty dates 50 | * @since 0.4.1 51 | * @param {Object} [grouped={}] 52 | * @param {Object} [tagged={}] 53 | * @param {Object} [regrouped={}] 54 | * @return {Object} regrouped 55 | */ 56 | _regroupTags(grouped = {}, tagged = {}, regrouped = {}) { 57 | this.loopResults((v, name) => { 58 | grouped[name] = groupBy(v, 'tags') 59 | tagged[name] = Object.keys(grouped[name]).map(tag => tag.split(',')) 60 | regrouped[name] = {} 61 | }) 62 | 63 | // scoped vars for following loops 64 | const tagKeys = Object.keys(tagged) 65 | let tagIndex = 0 66 | 67 | // go through each test (by name) 68 | mapown(tagged, (tags, testNameAsTag) => { 69 | const key = tagKeys[tagIndex] 70 | 71 | // go through each test case (by tag index) 72 | mapown(tags, (tag, index, obj) => { 73 | // setup 74 | let uniqTags = tag.slice(0) 75 | const tagStr = tag.join(',') 76 | 77 | // get tests for first test for first time tag was used 78 | const tests = grouped[testNameAsTag][tagStr] // [ti] 79 | 80 | if (!tests) { 81 | log 82 | .data({ 83 | group: grouped[testNameAsTag], 84 | atTag: grouped[testNameAsTag][tagStr], 85 | tagStr, 86 | testNameAsTag, 87 | tagIndex, 88 | }) 89 | .red('no tests somehow when tagging') 90 | .echo() 91 | return 92 | } 93 | 94 | let firstTest = tests[tagIndex] 95 | 96 | // @NOTE: @TODO: 97 | // was putting data on at complete, 98 | // not at cycle, 99 | // so data needs transforming 100 | if (!firstTest || !firstTest.now) return 101 | 102 | // prettify 103 | const uniqTagStr = uniqTags 104 | .map(t => log.colored(' ' + t + ' ', 'bgBlack.yellow')) 105 | .join(', ') 106 | 107 | const uniqKey = uniqTagStr + ' @' + prettydate(new Date(firstTest.now)) 108 | 109 | log.data({uniqKey, uniqTagStr, uniqTags}).echo(this.debug) 110 | 111 | // change the keys 112 | regrouped[testNameAsTag][uniqKey] = tests 113 | }) 114 | 115 | tagIndex++ 116 | }) 117 | 118 | return regrouped 119 | } 120 | 121 | /** 122 | * @protected 123 | * @since 0.4.1 124 | * @param {Object} regrouped results indexed by tagged 125 | * @param {Array} [tds=[]] 126 | * @return {Array} table rows 127 | */ 128 | _makeTagTableRows(regrouped, tds = []) { 129 | forown(regrouped, (v, name) => { 130 | const groups = Object.keys(regrouped[name]) 131 | const ref = this.getResultsWithNames 132 | 133 | forown(regrouped[name], (groupName, group, obj, i) => { 134 | this.parent.getResultsWithNames = () => ({ 135 | names: groups, 136 | results: obj, 137 | }) 138 | 139 | const avgs = this.avgs() 140 | 141 | const td = mapown(avgs, (avg, tag) => { 142 | const pretty = tag.split('@') 143 | const pureTag = pretty.shift() 144 | const time = pretty.pop() 145 | avg = log.colored(avg, this.colorForAvg(avg)) 146 | return {[name]: [pureTag, avg, time]} 147 | }) 148 | 149 | tds = tds.concat(td) 150 | }) 151 | 152 | // restore 153 | this.parent.getResultsWithNames = ref 154 | }) 155 | 156 | return tds 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /test/adding.js: -------------------------------------------------------------------------------- 1 | /* eslint no-return-await: "off" */ 2 | const test = require('ava') 3 | const {fosho} = require('fosho') 4 | const Bench = require('../src') 5 | 6 | const sleep = sleepDuration => 7 | new Promise(resolve => setTimeout(resolve, sleepDuration)) 8 | 9 | test('can add bench cases', t => { 10 | const bench = new Bench() 11 | bench 12 | .dir(__dirname) 13 | .filename('configstore-adding-test-basic') 14 | .add('case1', () => 1 + 1) 15 | .add('case2', () => 1 * 1) 16 | .add('case3', () => 1 / 1) 17 | 18 | fosho(bench.suite, t).obj() 19 | t.pass() 20 | }) 21 | 22 | test('can add async bench cases', t => { 23 | const bench = Bench.init(__dirname, 'configstore-adding-test-async') 24 | .addAsync('case1', async () => await sleep(100)) 25 | .addAsync('case2', async () => await sleep(200)) 26 | .addAsync('case31', async () => await sleep(100)) 27 | 28 | fosho(bench.suite, t).obj() 29 | t.pass() 30 | }) 31 | -------------------------------------------------------------------------------- /test/chain.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const {fosho} = require('fosho') 3 | const Bench = require('../src') 4 | 5 | test('can instantiate', t => { 6 | const record = new Bench(__dirname) 7 | t.true(record instanceof Bench) 8 | }) 9 | 10 | test('can chain filename and dir', t => { 11 | const bench = new Bench() 12 | bench.dir(__dirname).filename('configstore-adding-test-chain') 13 | fosho(bench.get('dir'), t).isString() 14 | }) 15 | 16 | test('can use init insteadof chain', t => { 17 | const bench = Bench.init(__dirname, 'configstore-adding-test-chain') 18 | fosho(bench.get('dir'), t).isString() 19 | }) 20 | -------------------------------------------------------------------------------- /test/cli.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const Bench = require('../src') 3 | 4 | test.todo('can run multiple times') 5 | test.todo('can dry run') 6 | test.todo('can debug') 7 | test.todo('can debug') 8 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const {fosho} = require('fosho') 3 | const Bench = require('../src') 4 | 5 | test.todo('benchmark reports over time') 6 | test.todo('benchmark test ranges') 7 | test.todo('benchmark stdout with flags') 8 | test.todo('benchmark onCycles contain ...data...') 9 | test.todo('can subscribe to lifecycle events') 10 | 11 | test.failing('can add multiple suites', t => { 12 | Bench.init() 13 | .dir(__dirname) 14 | .addSuite('one') 15 | .filename('configstore-adding-test') 16 | .add('bench1') 17 | .add('bench2') 18 | 19 | t.fail() 20 | }) 21 | -------------------------------------------------------------------------------- /test/persistance.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const {fosho} = require('fosho') 3 | const Bench = require('../src') 4 | 5 | test.todo('configstore') 6 | test.todo('file') 7 | -------------------------------------------------------------------------------- /test/running.js: -------------------------------------------------------------------------------- 1 | /* eslint no-return-await: "off" */ 2 | const test = require('ava') 3 | const {fosho, log} = require('fosho') 4 | const Bench = require('../src') 5 | 6 | const sleep = sleepDuration => 7 | new Promise(resolve => setTimeout(resolve, sleepDuration)) 8 | 9 | test('can run bench cases', t => { 10 | const bench = new Bench() 11 | bench 12 | .dir(__dirname) 13 | .filename('configstore.running-test-basic') 14 | .add('case1', () => 1 + 1) 15 | .add('case2', () => 1 * 1) 16 | .add('case3', () => 1 / 1) 17 | .run() 18 | 19 | const data = bench.getResults() 20 | fosho(data, t).obj() 21 | fosho(data, t).obj() 22 | fosho(data.case1, t).obj() 23 | }) 24 | 25 | test('can run async bench cases', t => { 26 | const bench = Bench.init(__dirname, 'configstore.running-test-async') 27 | .addAsync('case1', async () => await sleep(100)) 28 | .addAsync('case2', async () => await sleep(200)) 29 | .addAsync('case31', async () => await sleep(100)) 30 | .run() 31 | 32 | const data = bench.getResults() 33 | fosho(data, t).obj() 34 | fosho(data.case1, t).obj() 35 | fosho(data.case2, t).obj() 36 | fosho(data.case31, t).obj() 37 | }) 38 | --------------------------------------------------------------------------------