Skip to content

Commit 8daba22

Browse files
committed
feat(benchmark): add benchmark command
Adds `ng benchmark`, allowing the benchmark of commands. This is mostly a dev tool. Discussion is still needed to determine if it should be added.
1 parent e5ef996 commit 8daba22

File tree

5 files changed

+320
-0
lines changed

5 files changed

+320
-0
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,13 @@
103103
"source-map": "^0.5.6",
104104
"source-map-loader": "^0.1.5",
105105
"sourcemap-istanbul-instrumenter-loader": "^0.2.0",
106+
"strip-ansi": "^3.0.1",
106107
"style-loader": "^0.13.1",
107108
"stylus": "^0.54.5",
108109
"stylus-loader": "^2.1.0",
109110
"temp": "0.8.3",
110111
"through": "^2.3.6",
112+
"tree-kill": "^1.1.0",
111113
"tslint": "^4.0.2",
112114
"tslint-loader": "^3.3.0",
113115
"typescript": "~2.0.3",

packages/angular-cli/addon/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ module.exports = {
3333
'completion': require('../commands/completion').default,
3434
'doc': require('../commands/doc').default,
3535
'github-pages-deploy': require('../commands/github-pages-deploy').default,
36+
'benchmark': require('../commands/benchmark').default,
3637

3738
// Easter eggs.
3839
'make-this-awesome': require('../commands/easter-egg').default,
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
const Command = require('../ember-cli/lib/models/command');
2+
import { CliConfig } from '../models/config';
3+
import * as path from 'path';
4+
5+
export interface BenchmarkOptions {
6+
command: string;
7+
comment: string;
8+
iterations: number;
9+
extraArgs: string[];
10+
editFile: string;
11+
editString: string;
12+
match: string;
13+
matchCount: number;
14+
logFile: string;
15+
debug: boolean;
16+
}
17+
18+
const BenchmarkCommand = Command.extend({
19+
name: 'benchmark',
20+
description: 'Benchmark command run times',
21+
works: 'insideProject',
22+
availableOptions: [
23+
{ name: 'command', type: String, aliases: ['c'] },
24+
{ name: 'comment', type: String, aliases: ['cm'] },
25+
{ name: 'iterations', type: Number, default: 5, aliases: ['i'] },
26+
{ name: 'extra-args', type: Array, default: [], aliases: ['ea'] },
27+
{ name: 'edit-file', type: 'Path', aliases: ['ef'] },
28+
{ name: 'edit-string', type: String, aliases: ['es'] },
29+
{ name: 'match', type: String, aliases: ['m'] },
30+
{ name: 'match-count', type: Number, aliases: ['mc'] },
31+
{ name: 'log-file', type: 'Path', aliases: ['lf'] },
32+
{ name: 'debug', type: Boolean, default: false, aliases: ['-d'] },
33+
],
34+
35+
run: function (benchmarkOptions: BenchmarkOptions) {
36+
this.project.ngConfig = this.project.ngConfig || CliConfig.fromProject();
37+
const appConfig = this.project.ngConfig.config.apps[0];
38+
39+
// default to benchmarking rebuilds
40+
if (!benchmarkOptions.command) {
41+
benchmarkOptions.command = 'ng serve --no-progress';
42+
benchmarkOptions.match = 'Time: (.*)ms';
43+
benchmarkOptions.matchCount = 4;
44+
benchmarkOptions.editFile = `${appConfig.root}/${appConfig.main}`;
45+
benchmarkOptions.editString = '\'benchmark string\';';
46+
}
47+
48+
if (benchmarkOptions.editFile) {
49+
benchmarkOptions.editFile = path.resolve('./', benchmarkOptions.editFile);
50+
}
51+
52+
if (benchmarkOptions.command.match(/^ng (build|serve)/)
53+
&& benchmarkOptions.command.match(/--progress/) !== null
54+
) {
55+
this.ui.writeLine('Auto-added \'--no-progress\' to build/serve command.');
56+
benchmarkOptions.command = benchmarkOptions.command + ' --no-progress';
57+
}
58+
59+
const BenchmarkTask = require('../tasks/benchmark').BenchmarkTask;
60+
const benchmarkTask = new BenchmarkTask({
61+
ui: this.ui,
62+
project: this.project
63+
});
64+
65+
return benchmarkTask.run(benchmarkOptions);
66+
}
67+
});
68+
69+
70+
export default BenchmarkCommand;

packages/angular-cli/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,13 @@
8585
"silent-error": "^1.0.0",
8686
"source-map-loader": "^0.1.5",
8787
"sourcemap-istanbul-instrumenter-loader": "^0.2.0",
88+
"strip-ansi": "^3.0.1",
8889
"style-loader": "^0.13.1",
8990
"stylus": "^0.54.5",
9091
"stylus-loader": "^2.1.0",
9192
"temp": "0.8.3",
9293
"through": "^2.3.6",
94+
"tree-kill": "^1.1.0",
9395
"tslint": "^4.0.2",
9496
"tslint-loader": "^3.3.0",
9597
"typescript": "~2.0.3",
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
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

Comments
 (0)