|
| 1 | +import { BenchmarkOptions } from '../commands/benchmark'; |
| 2 | +const Task = require('../ember-cli/lib/models/task'); |
| 3 | +const spawn = require('child_process').spawn; |
| 4 | +const stripAnsi = require('strip-ansi'); |
| 5 | +const treeKill = require('tree-kill'); |
| 6 | +const fs = require('fs'); |
| 7 | + |
| 8 | + |
| 9 | +export const BenchmarkTask = Task.extend({ |
| 10 | + run: function (benchmarkOptions: BenchmarkOptions) { |
| 11 | + const ui = this.ui; |
| 12 | + |
| 13 | + function log(str: string) { |
| 14 | + ui.writeLine(str); |
| 15 | + if (benchmarkOptions.logFile) { |
| 16 | + fs.appendFileSync(benchmarkOptions.logFile, str + '\n'); |
| 17 | + } |
| 18 | + } |
| 19 | + |
| 20 | + const cwd = process.env.PWD; |
| 21 | + |
| 22 | + // Build match function |
| 23 | + let matchFn: any = null; |
| 24 | + if (benchmarkOptions.match) { |
| 25 | + matchFn = makeMatchFn( |
| 26 | + benchmarkOptions.debug, |
| 27 | + benchmarkOptions.match, |
| 28 | + benchmarkOptions.matchCount, |
| 29 | + benchmarkOptions.editFile, |
| 30 | + benchmarkOptions.editString, |
| 31 | + ); |
| 32 | + } |
| 33 | + |
| 34 | + let editedFileContents: string; |
| 35 | + if (benchmarkOptions.editFile) { |
| 36 | + // backup contents of file that is being edited for rebuilds |
| 37 | + editedFileContents = fs.readFileSync(benchmarkOptions.editFile, 'utf8'); |
| 38 | + } |
| 39 | + // combine flags commands |
| 40 | + let flagCombinations = combine(benchmarkOptions.extraArgs); |
| 41 | + flagCombinations.unshift([]); |
| 42 | + |
| 43 | + |
| 44 | + const startTime = Date.now(); |
| 45 | + |
| 46 | + log(`Base command: ${benchmarkOptions.command}`); |
| 47 | + log(`Iterations: ${benchmarkOptions.iterations}`); |
| 48 | + if (benchmarkOptions.comment) { |
| 49 | + log(`Comment: ${benchmarkOptions.comment}`); |
| 50 | + } |
| 51 | + if (benchmarkOptions.extraArgs.length > 0) { |
| 52 | + log(`Extra args: ${benchmarkOptions.extraArgs}`); |
| 53 | + } |
| 54 | + log(''); |
| 55 | + |
| 56 | + |
| 57 | + let promise = Promise.resolve(); |
| 58 | + |
| 59 | + flagCombinations.forEach((flags) => |
| 60 | + promise = promise |
| 61 | + .then(() => serialMultiPromise( |
| 62 | + benchmarkOptions.iterations, |
| 63 | + matchSpawn, |
| 64 | + benchmarkOptions.debug, |
| 65 | + cwd, |
| 66 | + matchFn, |
| 67 | + benchmarkOptions.command, |
| 68 | + flags |
| 69 | + ).then((results: any[]) => { |
| 70 | + const failures = results.filter(result => result.error); |
| 71 | + log(`Full command: ${benchmarkOptions.command} ${flags.join(' ')}`); |
| 72 | + |
| 73 | + let times = results.filter(result => !result.error) |
| 74 | + .map((result) => result.time); |
| 75 | + log(`Time average: ${average(times)}`); |
| 76 | + log(`Times: ${times.join()}`); |
| 77 | + |
| 78 | + if (benchmarkOptions.match) { |
| 79 | + let matches = results.filter(result => !result.error) |
| 80 | + .map((result) => result.match); |
| 81 | + if (matches.every(match => isNumber(match))) { |
| 82 | + log(`Match average: ${average(matches)}`); |
| 83 | + } |
| 84 | + log(`Matches: ${matches.join()}`); |
| 85 | + } |
| 86 | + |
| 87 | + if (failures.length > 0) { log(`Failures: ${failures.length}`); } |
| 88 | + log(''); |
| 89 | + })) |
| 90 | + ); |
| 91 | + |
| 92 | + return promise.then(() => { |
| 93 | + log(`Benchmark execution time: ${Date.now() - startTime}ms`); |
| 94 | + // restore contents of file that was being edited for rebuilds |
| 95 | + if (benchmarkOptions.editFile) { |
| 96 | + fs.writeFileSync(benchmarkOptions.editFile, editedFileContents, 'utf8'); |
| 97 | + } |
| 98 | + }); |
| 99 | + } |
| 100 | +}); |
| 101 | + |
| 102 | + |
| 103 | +// Determine is a value is a number |
| 104 | +function isNumber(value: any) { |
| 105 | + return !isNaN(parseFloat(value)) && isFinite(value); |
| 106 | +} |
| 107 | + |
| 108 | +// Returns average of all numbers in `arr` |
| 109 | +function average(arr: any[]) { |
| 110 | + return arr.reduce((prev, curr) => prev + Number(curr), 0) / arr.length; |
| 111 | +} |
| 112 | + |
| 113 | +// Returns an array of `length` length, filled with `undefined` |
| 114 | +function makeArray(length: number) { |
| 115 | + return Array.apply(null, Array(length)); |
| 116 | +} |
| 117 | + |
| 118 | +// Make all combinations of array elements |
| 119 | +// http://stackoverflow.com/a/5752056/2116927 |
| 120 | +function combine(array: any[]) { |
| 121 | + let fn = function (n: number, src: any[], got: any[], all: any[]) { |
| 122 | + if (n == 0) { |
| 123 | + if (got.length > 0) { |
| 124 | + all[all.length] = got; |
| 125 | + } |
| 126 | + return; |
| 127 | + } |
| 128 | + for (let j = 0; j < src.length; j++) { |
| 129 | + fn(n - 1, src.slice(j + 1), got.concat([src[j]]), all); |
| 130 | + } |
| 131 | + return; |
| 132 | + }; |
| 133 | + let all: any[] = []; |
| 134 | + for (let i = 0; i < array.length; i++) { |
| 135 | + fn(i, array, [], all); |
| 136 | + } |
| 137 | + if (array.length > 0) { all.push(array); } |
| 138 | + return all; |
| 139 | +} |
| 140 | + |
| 141 | +// Returns a promise with the result of calling `times` times the `fn` function with `args` |
| 142 | +// `fn` will be called in parallel, and is expected to return a promise |
| 143 | +// Not used currently |
| 144 | +function parallelMultiPromise(times: number, fn: any, ...args: string[]) { |
| 145 | + return Promise.all(makeArray(times).map(() => fn.apply(null, args))); |
| 146 | +} |
| 147 | + |
| 148 | +// Returns a promise with the result of calling `times` times the `fn` function with `args` |
| 149 | +// `fn` will be called in serial, and is expected to return a promise |
| 150 | +function serialMultiPromise(times: number, fn: any, ...args: any[]) { |
| 151 | + let results: Promise<any>[] = []; |
| 152 | + let promise = Promise.resolve(); |
| 153 | + makeArray(times).forEach(() => promise = promise.then(() => |
| 154 | + fn.apply(null, args).then((result: Promise<any>) => results.push(result)) |
| 155 | + )); |
| 156 | + return promise.then(() => results); |
| 157 | +} |
| 158 | + |
| 159 | +// Spawns `cmd` with `args` |
| 160 | +// Calls `matchFn` with the `stdout` output, a `result` var and the process |
| 161 | +// `dataFn` is expected to modify `result` |
| 162 | +function matchSpawn(debug: boolean, cwd: string, matchFn: any, cmd: string, args: string[] = []) { |
| 163 | + // dataFn will have access to result and use it to store results |
| 164 | + let result = { |
| 165 | + // overrideErr will signal that an error code on the exit event should be ignored |
| 166 | + // This is useful on windows where killing a tree of processes always makes them |
| 167 | + // exit with an error code |
| 168 | + overrideErr: false, |
| 169 | + time: 0 |
| 170 | + }; |
| 171 | + let stdout = ''; |
| 172 | + let spawnOptions: any = { |
| 173 | + cwd: cwd, |
| 174 | + env: process.env |
| 175 | + }; |
| 176 | + |
| 177 | + if (process.platform.startsWith('win')) { |
| 178 | + args = ['/c', cmd].concat(args); |
| 179 | + cmd = 'cmd.exe'; |
| 180 | + spawnOptions['stdio'] = 'pipe'; |
| 181 | + } |
| 182 | + |
| 183 | + const startTime = Date.now(); |
| 184 | + const childProcess = spawn(cmd, args, spawnOptions); |
| 185 | + |
| 186 | + childProcess.stdout.on('data', (data: Buffer) => { |
| 187 | + let strippedData = stripAnsi(data.toString('utf-8')); |
| 188 | + if (debug) { |
| 189 | + console.log(strippedData); |
| 190 | + } |
| 191 | + stdout += strippedData; |
| 192 | + if (matchFn) { |
| 193 | + matchFn(strippedData, result, childProcess); |
| 194 | + } |
| 195 | + }); |
| 196 | + |
| 197 | + return new Promise((resolve, reject) => |
| 198 | + childProcess.on('exit', (err: number) => { |
| 199 | + result.time = Date.now() - startTime; |
| 200 | + return err && !result.overrideErr |
| 201 | + ? resolve({ err, stdout }) |
| 202 | + : resolve(result); |
| 203 | + }) |
| 204 | + ); |
| 205 | +} |
| 206 | + |
| 207 | +// Makes a function that matches a RegExp to the process output, and then kills it |
| 208 | +// Optionally matches `matchCount` times, and edits `editFile` by appending `editString` |
| 209 | +function makeMatchFn( |
| 210 | + debug: boolean, |
| 211 | + match: string, |
| 212 | + matchCount: number, |
| 213 | + editFile: string, |
| 214 | + editString: string |
| 215 | +) { |
| 216 | + return function (data: string, result: any, childProcess: NodeJS.Process) { |
| 217 | + let localMatch = data.match(new RegExp(match)); |
| 218 | + |
| 219 | + if (debug) { |
| 220 | + console.log('Match output:', localMatch); |
| 221 | + } |
| 222 | + |
| 223 | + const killProcess = (childProcess: NodeJS.Process) => { |
| 224 | + result.overrideErr = true; |
| 225 | + treeKill(childProcess.pid); |
| 226 | + }; |
| 227 | + if (localMatch) { |
| 228 | + let firstMatch = localMatch[1]; |
| 229 | + if (!matchCount) { |
| 230 | + result.match = firstMatch; |
| 231 | + killProcess(childProcess); |
| 232 | + } else { |
| 233 | + result.counter = result.counter ? result.counter + 1 : 1; |
| 234 | + if (result.counter < matchCount) { |
| 235 | + if (editFile) { |
| 236 | + fs.appendFileSync(editFile, editString); |
| 237 | + } |
| 238 | + } else { |
| 239 | + result.match = firstMatch; |
| 240 | + killProcess(childProcess); |
| 241 | + } |
| 242 | + } |
| 243 | + } |
| 244 | + }; |
| 245 | +} |
0 commit comments