Skip to content

Commit 8ae89b6

Browse files
committed
feat(@ngtools/benchmark): add benchmark package
Adds `@ngtools/benchmark` allowing the benchmark of commands via `ngbench` binary.
1 parent 9760ef9 commit 8ae89b6

File tree

17 files changed

+555
-3
lines changed

17 files changed

+555
-3
lines changed

.travis.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ matrix:
1212
allow_failures:
1313
- env: NODE_SCRIPT="tests/run_e2e.js --nightly"
1414
- env: NODE_SCRIPT="tests/run_e2e.js --ng2"
15+
- env: NODE_SCRIPT="tests/run_e2e.js --glob=benchmarks/**"
1516
- node_js: "7"
1617
include:
1718
- node_js: "6"
@@ -40,6 +41,9 @@ matrix:
4041
- node_js: "6"
4142
os: linux
4243
env: NODE_SCRIPT="tests/run_e2e.js --nightly"
44+
- node_js: "6"
45+
os: linux
46+
env: NODE_SCRIPT="tests/run_e2e.js --glob=benchmarks/**"
4347
- node_js: "7"
4448
os: linux
4549
env: NODE_SCRIPT=tests/run_e2e.js

bin/ngbench

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env node
2+
'use strict';
3+
4+
// Provide a title to the process in `ps`
5+
process.title = '@ngtools/benchmark';
6+
7+
require('../lib/bootstrap-local');
8+
require('../packages/@ngtools/benchmark/bin/ngbench');

package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"main": "packages/@angular/cli/lib/cli/index.js",
66
"trackingCode": "UA-8594346-19",
77
"bin": {
8-
"ng": "./bin/ng"
8+
"ng": "./bin/ng",
9+
"ngbench": "./bin/ngbench"
910
},
1011
"keywords": [],
1112
"scripts": {
@@ -87,18 +88,21 @@
8788
"silent-error": "^1.0.0",
8889
"source-map": "^0.5.6",
8990
"source-map-loader": "^0.2.0",
91+
"strip-ansi": "^3.0.1",
9092
"style-loader": "^0.13.1",
9193
"stylus": "^0.54.5",
9294
"stylus-loader": "^3.0.1",
9395
"temp": "0.8.3",
9496
"typescript": "~2.3.1",
97+
"tree-kill": "^1.1.0",
9598
"url-loader": "^0.5.7",
9699
"walk-sync": "^0.3.1",
97100
"webpack": "~2.4.0",
98101
"webpack-dev-middleware": "^1.10.2",
99102
"webpack-dev-server": "~2.4.5",
100103
"webpack-merge": "^2.4.0",
101-
"zone.js": "^0.8.4"
104+
"zone.js": "^0.8.4",
105+
"yargs": "^6.6.0"
102106
},
103107
"ember-addon": {
104108
"paths": [
@@ -126,6 +130,7 @@
126130
"@types/semver": "^5.3.30",
127131
"@types/source-map": "^0.5.0",
128132
"@types/webpack": "^2.2.15",
133+
"@types/yargs": "^6.5.0",
129134
"chai": "^3.5.0",
130135
"conventional-changelog": "^1.1.0",
131136
"dtsgenerator": "^0.9.1",
@@ -146,7 +151,7 @@
146151
"sinon": "^1.17.3",
147152
"spdx-satisfies": "^0.1.3",
148153
"through": "^2.3.6",
149-
"tree-kill": "^1.0.0",
154+
"tree-kill": "^1.1.0",
150155
"ts-node": "^2.0.0",
151156
"tslint": "^5.1.0"
152157
},
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/usr/bin/env node
2+
require('../src/index');
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "@ngtools/benchmark",
3+
"version": "0.0.0",
4+
"description": "",
5+
"main": "./src/index.js",
6+
"bin": {
7+
"ngbench": "./bin/ngbench"
8+
},
9+
"license": "MIT",
10+
"dependencies": {
11+
"@ngtools/logger": "^0.1.4",
12+
"chalk": "^1.1.3",
13+
"lodash": "^4.11.1",
14+
"rxjs": "^5.0.1",
15+
"strip-ansi": "^3.0.1",
16+
"tree-kill": "^1.1.0",
17+
"yargs": "^6.6.0"
18+
}
19+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export interface BenchmarkOptions {
2+
command: string;
3+
iterations: number;
4+
extraArgs: string[];
5+
match: string;
6+
matchCount: number;
7+
matchEditFile: string;
8+
matchEditString: string;
9+
comment: string;
10+
logFile: string;
11+
debug: boolean;
12+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { Logger } from '@ngtools/logger';
2+
import * as fs from 'fs';
3+
const mean = require('lodash/mean');
4+
const toNumber = require('lodash/toNumber');
5+
6+
7+
import { BenchmarkOptions } from './benchmark-options';
8+
import {
9+
combine,
10+
makeMatchFn,
11+
matchSpawn,
12+
serialMultiPromise
13+
} from './utils';
14+
15+
export function benchmark(benchmarkOptions: BenchmarkOptions, logger: Logger) {
16+
const logIfDefined = (str: string, prop: any) => {
17+
if (Array.isArray(prop) && prop.length === 0) {
18+
prop = null;
19+
}
20+
return prop ? logger.info(str + prop) : null;
21+
};
22+
23+
const cwd = process.cwd();
24+
25+
// Build match function
26+
let matchFn: any = null;
27+
if (benchmarkOptions.match) {
28+
matchFn = makeMatchFn(
29+
logger,
30+
benchmarkOptions.match,
31+
benchmarkOptions.matchCount,
32+
benchmarkOptions.matchEditFile,
33+
benchmarkOptions.matchEditString,
34+
);
35+
}
36+
37+
let editedFileContents: string;
38+
if (benchmarkOptions.matchEditFile) {
39+
// backup contents of file that is being edited for rebuilds
40+
editedFileContents = fs.readFileSync(benchmarkOptions.matchEditFile, 'utf8');
41+
}
42+
// combine flags commands
43+
let flagCombinations = combine(benchmarkOptions.extraArgs);
44+
flagCombinations.unshift([]);
45+
46+
const startTime = Date.now();
47+
logger.info(`Base command: ${benchmarkOptions.command}`);
48+
logger.info(`Iterations: ${benchmarkOptions.iterations}`);
49+
logIfDefined('Comment: ', benchmarkOptions.comment);
50+
logIfDefined('Extra args: ', benchmarkOptions.extraArgs);
51+
logIfDefined('Logging to: ', benchmarkOptions.logFile);
52+
if (benchmarkOptions.match) {
53+
logger.info(`Match output: ${benchmarkOptions.match}`);
54+
logIfDefined('Match count: ', benchmarkOptions.matchCount);
55+
logIfDefined('Match edit file: ', benchmarkOptions.matchEditFile);
56+
logIfDefined('Match edit string: ', benchmarkOptions.matchEditString);
57+
}
58+
if (benchmarkOptions.debug) {
59+
logger.debug('### Debug mode, all output is logged ###');
60+
}
61+
logger.info('');
62+
63+
64+
let promise = Promise.resolve();
65+
let hasFailures = false;
66+
67+
flagCombinations.forEach((flags) =>
68+
promise = promise
69+
.then(() => serialMultiPromise(
70+
benchmarkOptions.iterations,
71+
matchSpawn,
72+
logger,
73+
cwd,
74+
matchFn,
75+
benchmarkOptions.command,
76+
flags
77+
).then((results: any[]) => {
78+
const failures = results.filter(result => result.err && result.err !== 0);
79+
logger.info(`Full command: ${benchmarkOptions.command} ${flags.join(' ')}`);
80+
81+
let times = results.filter(result => !result.err)
82+
.map((result) => result.time);
83+
logger.info(`Time average: ${mean(times)}`);
84+
logger.info(`Times: ${times.join()}`);
85+
86+
if (benchmarkOptions.match) {
87+
let matches = results.filter(result => !result.err)
88+
.map((result) => result.match);
89+
90+
let matchesAsNumbers = matches.map(toNumber);
91+
if (matches.every(match => match != NaN)) {
92+
logger.info(`Match average: ${mean(matchesAsNumbers)}`);
93+
}
94+
logger.info(`Matches: ${matches.join()}`);
95+
}
96+
97+
if (failures.length > 0) {
98+
hasFailures = true;
99+
logger.info(`Failures: ${failures.length}`);
100+
logger.info(JSON.stringify(failures));
101+
}
102+
logger.info('');
103+
}))
104+
);
105+
106+
return promise.then(() => {
107+
logger.info(`Benchmark execution time: ${Date.now() - startTime}ms`);
108+
// restore contents of file that was being edited for rebuilds
109+
if (benchmarkOptions.matchEditFile) {
110+
fs.writeFileSync(benchmarkOptions.matchEditFile, editedFileContents, 'utf8');
111+
}
112+
return hasFailures ? Promise.reject(new Error('Some benchmarks failed')) : Promise.resolve();
113+
});
114+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { Logger, LogEntry } from '@ngtools/logger';
2+
import { bold, red, yellow, white } from 'chalk';
3+
import * as fs from 'fs';
4+
import * as path from 'path';
5+
import * as yargs from 'yargs';
6+
import 'rxjs/add/operator/filter';
7+
8+
import { BenchmarkOptions } from './benchmark-options';
9+
import { benchmark } from './benchmark';
10+
11+
12+
// Set options via yargs
13+
const benchmarkOptions: BenchmarkOptions = yargs
14+
.usage('$0 [args]')
15+
.options({
16+
'command': {
17+
description: 'Command to benchmark, defaults to benchmarking `ng serve` if not set',
18+
type: 'string',
19+
alias: 'c'
20+
},
21+
'iterations': {
22+
description: 'Number of iterations to run benchmark',
23+
type: 'number',
24+
default: 5,
25+
alias: 'i'
26+
},
27+
'extra-args': {
28+
description: 'Extra arguments to combine and run',
29+
type: 'array',
30+
default: [],
31+
alias: 'ea'
32+
},
33+
'match': {
34+
description: 'Command output to match',
35+
type: 'string',
36+
alias: 'm'
37+
},
38+
'match-count': {
39+
description: 'Times to match output',
40+
type: 'number',
41+
alias: 'mc'
42+
},
43+
'match-edit-file': {
44+
description: 'File to edit after a output match',
45+
type: 'string',
46+
alias: 'mef'
47+
},
48+
'match-edit-string': {
49+
description: 'String to use in --match-edit-file',
50+
type: 'string',
51+
alias: 'mes'
52+
},
53+
'comment': {
54+
description: 'Comment to add to output',
55+
type: 'string',
56+
alias: 'cm'
57+
},
58+
'log-file': {
59+
description: 'File to log output',
60+
type: 'string',
61+
alias: 'lf'
62+
},
63+
'debug': {
64+
description: 'Show command output',
65+
type: 'boolean',
66+
default: false,
67+
alias: 'd'
68+
},
69+
})
70+
.help()
71+
.argv;
72+
73+
// Initialize logger
74+
const logger = new Logger('ngbench');
75+
76+
logger
77+
.filter((entry: LogEntry) => (entry.level != 'debug' || benchmarkOptions.debug))
78+
.subscribe((entry: LogEntry) => {
79+
let color: (s: string) => string = white;
80+
let output = process.stdout;
81+
switch (entry.level) {
82+
case 'info': color = white; break;
83+
case 'warn': color = yellow; break;
84+
case 'error': color = red; output = process.stderr; break;
85+
case 'fatal': color = (x: string) => bold(red(x)); output = process.stderr; break;
86+
}
87+
88+
output.write(color(entry.message) + '\n');
89+
if (benchmarkOptions.logFile) {
90+
fs.appendFileSync(benchmarkOptions.logFile, entry.message + '\n');
91+
}
92+
});
93+
94+
logger
95+
.filter((entry: LogEntry) => entry.level == 'fatal')
96+
.subscribe(() => {
97+
process.stderr.write('A fatal error happened. See details above.');
98+
process.exit(1);
99+
});
100+
101+
102+
// Set compound defauls and resolve paths
103+
if (!benchmarkOptions.command) {
104+
benchmarkOptions.command = 'ng serve --no-progress';
105+
benchmarkOptions.match = benchmarkOptions.match || 'Time: (.*)ms';
106+
benchmarkOptions.matchCount = benchmarkOptions.matchCount || 4;
107+
benchmarkOptions.matchEditFile = benchmarkOptions.matchEditFile || 'src/main.ts';
108+
benchmarkOptions.matchEditString = benchmarkOptions.matchEditString || 'console.log(1);';
109+
}
110+
111+
if (benchmarkOptions.matchEditFile) {
112+
benchmarkOptions.matchEditFile = path.resolve('./', benchmarkOptions.matchEditFile);
113+
}
114+
115+
if (benchmarkOptions.command.match(/^ng (build|serve)/)
116+
&& benchmarkOptions.command.match(/--no-progress/) === null
117+
) {
118+
logger.warn('Auto-added \'--no-progress\' to build/serve command.');
119+
benchmarkOptions.command = benchmarkOptions.command + ' --no-progress';
120+
}
121+
122+
// Run benchmark
123+
benchmark(benchmarkOptions, logger)
124+
.catch((err) => {
125+
logger.fatal(JSON.stringify(err));
126+
});

0 commit comments

Comments
 (0)