| 1 | 'use strict'; |
| 2 | |
| 3 | const chalk = require('chalk'); |
| 4 | const Table = require('cli-table'); |
| 5 | |
| 6 | function percentChange(prev, current, prevSem, currentSem) { |
| 7 | const [mean, sd] = calculateMeanAndSdOfRatioFromDeltaMethod( |
| 8 | prev, |
| 9 | current, |
| 10 | prevSem, |
| 11 | currentSem |
| 12 | ); |
| 13 | const pctChange = +(mean * 100).toFixed(1); |
| 14 | const ci95 = +(100 * 1.96 * sd).toFixed(1); |
| 15 | |
| 16 | const ciInfo = ci95 > 0 ? ` +- ${ci95} %` : ''; |
| 17 | const text = `${pctChange > 0 ? '+' : ''}${pctChange} %${ciInfo}`; |
| 18 | if (pctChange + ci95 < 0) { |
| 19 | return chalk.green(text); |
| 20 | } else if (pctChange - ci95 > 0) { |
| 21 | return chalk.red(text); |
| 22 | } else { |
| 23 | // Statistically insignificant. |
| 24 | return text; |
| 25 | } |
| 26 | } |
| 27 | |
| 28 | function calculateMeanAndSdOfRatioFromDeltaMethod( |
| 29 | meanControl, |
| 30 | meanTest, |
| 31 | semControl, |
| 32 | semTest |
| 33 | ) { |
| 34 | const mean = |
| 35 | (meanTest - meanControl) / meanControl - |
| 36 | (Math.pow(semControl, 2) * meanTest) / Math.pow(meanControl, 3); |
| 37 | const variance = |
| 38 | Math.pow(semTest / meanControl, 2) + |
| 39 | Math.pow(semControl * meanTest, 2) / Math.pow(meanControl, 4); |
| 40 | return [mean, Math.sqrt(variance)]; |
| 41 | } |
| 42 | |
| 43 | function addBenchmarkResults(table, localResults, remoteMasterResults) { |
| 44 | const benchmarks = Object.keys( |
| 45 | (localResults && localResults.benchmarks) || |
| 46 | (remoteMasterResults && remoteMasterResults.benchmarks) |
| 47 | ); |
| 48 | benchmarks.forEach(benchmark => { |
| 49 | const rowHeader = [chalk.white.bold(benchmark)]; |
| 50 | if (remoteMasterResults) { |
| 51 | rowHeader.push(chalk.white.bold('Time')); |
| 52 | } |
| 53 | if (localResults) { |
| 54 | rowHeader.push(chalk.white.bold('Time')); |
| 55 | } |
| 56 | if (localResults && remoteMasterResults) { |
| 57 | rowHeader.push(chalk.white.bold('Diff')); |
| 58 | } |
| 59 | table.push(rowHeader); |
| 60 | |
| 61 | const measurements = |
| 62 | (localResults && localResults.benchmarks[benchmark].averages) || |
| 63 | (remoteMasterResults && |
| 64 | remoteMasterResults.benchmarks[benchmark].averages); |
| 65 | measurements.forEach((measurement, i) => { |
| 66 | const row = [chalk.gray(measurement.entry)]; |
| 67 | let remoteMean; |
| 68 | let remoteSem; |
| 69 | if (remoteMasterResults) { |
| 70 | remoteMean = remoteMasterResults.benchmarks[benchmark].averages[i].mean; |
| 71 | remoteSem = remoteMasterResults.benchmarks[benchmark].averages[i].sem; |
| 72 | // https://en.wikipedia.org/wiki/1.96 gives a 99% confidence interval. |
| 73 | const ci95 = remoteSem * 1.96; |
| 74 | row.push( |
| 75 | chalk.white(+remoteMean.toFixed(2) + ' ms +- ' + ci95.toFixed(2)) |
| 76 | ); |
| 77 | } |
| 78 | let localMean; |
| 79 | let localSem; |
| 80 | if (localResults) { |
| 81 | localMean = localResults.benchmarks[benchmark].averages[i].mean; |
| 82 | localSem = localResults.benchmarks[benchmark].averages[i].sem; |
| 83 | const ci95 = localSem * 1.96; |
| 84 | row.push( |
| 85 | chalk.white(+localMean.toFixed(2) + ' ms +- ' + ci95.toFixed(2)) |
| 86 | ); |
| 87 | } |
| 88 | if (localResults && remoteMasterResults) { |
| 89 | row.push(percentChange(remoteMean, localMean, remoteSem, localSem)); |
| 90 | } |
| 91 | table.push(row); |
| 92 | }); |
| 93 | }); |
| 94 | } |
| 95 | |
| 96 | function printResults(localResults, remoteMasterResults) { |
| 97 | const head = ['']; |
| 98 | if (remoteMasterResults) { |
| 99 | head.push(chalk.yellow.bold('Remote (Merge Base)')); |
| 100 | } |
| 101 | if (localResults) { |
| 102 | head.push(chalk.green.bold('Local (Current Branch)')); |
| 103 | } |
| 104 | if (localResults && remoteMasterResults) { |
| 105 | head.push(''); |
| 106 | } |
| 107 | const table = new Table({head}); |
| 108 | addBenchmarkResults(table, localResults, remoteMasterResults); |
| 109 | console.log(table.toString()); |
| 110 | } |
| 111 | |
| 112 | module.exports = printResults; |