From 06342e1ea740c6132605d77d8a5b4c9a30e800d2 Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Thu, 23 Aug 2018 11:19:29 +0100 Subject: [PATCH 1/4] feat(@angular-devkit/benchmark): add package --- .monorepo.json | 5 + bin/benchmark | 19 ++ package.json | 1 + packages/angular_devkit/benchmark/BUILD | 59 ++++++ packages/angular_devkit/benchmark/README.md | 116 +++++++++++ .../angular_devkit/benchmark/package.json | 24 +++ .../angular_devkit/benchmark/src/command.ts | 23 +++ .../benchmark/src/default-reporter.ts | 24 +++ .../benchmark/src/default-stats-capture.ts | 60 ++++++ .../src/default-stats-capture_spec.ts | 48 +++++ .../angular_devkit/benchmark/src/index.ts | 16 ++ .../benchmark/src/interfaces.d.ts | 49 +++++ packages/angular_devkit/benchmark/src/main.ts | 184 ++++++++++++++++++ .../angular_devkit/benchmark/src/main_spec.ts | 145 ++++++++++++++ .../benchmark/src/monitored-process.ts | 106 ++++++++++ .../benchmark/src/monitored-process_spec.ts | 50 +++++ .../benchmark/src/run-benchmark.ts | 98 ++++++++++ .../benchmark/src/test/exit-code-one.js | 1 + .../benchmark/src/test/fibonacci.js | 2 + .../benchmark/src/test/test-script.js | 7 + .../angular_devkit/benchmark/src/utils.ts | 49 +++++ yarn.lock | 10 + 22 files changed, 1096 insertions(+) create mode 100755 bin/benchmark create mode 100644 packages/angular_devkit/benchmark/BUILD create mode 100644 packages/angular_devkit/benchmark/README.md create mode 100644 packages/angular_devkit/benchmark/package.json create mode 100644 packages/angular_devkit/benchmark/src/command.ts create mode 100644 packages/angular_devkit/benchmark/src/default-reporter.ts create mode 100644 packages/angular_devkit/benchmark/src/default-stats-capture.ts create mode 100644 packages/angular_devkit/benchmark/src/default-stats-capture_spec.ts create mode 100644 packages/angular_devkit/benchmark/src/index.ts create mode 100644 packages/angular_devkit/benchmark/src/interfaces.d.ts create mode 100644 packages/angular_devkit/benchmark/src/main.ts create mode 100644 packages/angular_devkit/benchmark/src/main_spec.ts create mode 100644 packages/angular_devkit/benchmark/src/monitored-process.ts create mode 100644 packages/angular_devkit/benchmark/src/monitored-process_spec.ts create mode 100644 packages/angular_devkit/benchmark/src/run-benchmark.ts create mode 100644 packages/angular_devkit/benchmark/src/test/exit-code-one.js create mode 100644 packages/angular_devkit/benchmark/src/test/fibonacci.js create mode 100644 packages/angular_devkit/benchmark/src/test/test-script.js create mode 100644 packages/angular_devkit/benchmark/src/utils.ts diff --git a/.monorepo.json b/.monorepo.json index d9c426f5a664..2f1f149079ca 100644 --- a/.monorepo.json +++ b/.monorepo.json @@ -87,6 +87,11 @@ "hash": "51b6cc55b25fce87f6a92f0d0942f79c", "snapshotRepo": "angular/angular-devkit-architect-cli-builds" }, + "@angular-devkit/benchmark": { + "name": "Benchmark", + "section": "Tooling", + "version": "0.0.0" + }, "@angular-devkit/build-optimizer": { "name": "Build Optimizer", "links": [ diff --git a/bin/benchmark b/bin/benchmark new file mode 100755 index 000000000000..4ada663d8bfc --- /dev/null +++ b/bin/benchmark @@ -0,0 +1,19 @@ +#!/usr/bin/env node +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +'use strict'; + + +require('../lib/bootstrap-local'); +const packages = require('../lib/packages').packages; +const main = require(packages['@angular-devkit/benchmark'].bin['benchmark']).main; + +const args = process.argv.slice(2); +main({ args }) + .then(exitCode => process.exitCode = exitCode) + .catch(e => { throw (e); }); diff --git a/package.json b/package.json index 9f6c4df624c2..8affd2f1af92 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "description": "Software Development Kit for Angular", "bin": { "architect": "./bin/architect", + "benchmark": "./bin/benchmark", "build-optimizer": "./bin/build-optimizer", "devkit-admin": "./bin/devkit-admin", "ng": "./bin/ng", diff --git a/packages/angular_devkit/benchmark/BUILD b/packages/angular_devkit/benchmark/BUILD new file mode 100644 index 000000000000..3d914cf54edb --- /dev/null +++ b/packages/angular_devkit/benchmark/BUILD @@ -0,0 +1,59 @@ +# Copyright Google Inc. All Rights Reserved. +# +# Use of this source code is governed by an MIT-style license that can be +# found in the LICENSE file at https://angular.io/license + +licenses(["notice"]) # MIT + +load("@build_bazel_rules_typescript//:defs.bzl", "ts_library") +load("@build_bazel_rules_nodejs//:defs.bzl", "jasmine_node_test") + +package(default_visibility = ["//visibility:public"]) + +ts_library( + name = "benchmark", + srcs = glob( + include = ["src/**/*.ts"], + exclude = [ + "src/**/*_spec.ts", + "src/**/*_spec_large.ts", + "src/**/*_benchmark.ts", + ], + ), + module_name = "@angular-devkit/benchmark", + module_root = "src/index.d.ts", + deps = [ + "//packages/angular_devkit/core", + "@rxjs", + "@rxjs//operators", + # @typings: node + ], +) + +ts_library( + name = "benchmark_test_lib", + srcs = glob( + include = [ + "src/**/*_spec.ts", + "src/**/*_spec_large.ts", + ], + ), + data = [ + "src/test/exit-code-one.js", + "src/test/fibonacci.js", + "src/test/test-script.js", + ], + deps = [ + ":benchmark", + "//packages/angular_devkit/core", + "@rxjs", + "@rxjs//operators", + # @typings: jasmine + # @typings: node + ], +) + +jasmine_node_test( + name = "benchmark_test", + srcs = [":benchmark_test_lib"], +) \ No newline at end of file diff --git a/packages/angular_devkit/benchmark/README.md b/packages/angular_devkit/benchmark/README.md new file mode 100644 index 000000000000..caddc4c97942 --- /dev/null +++ b/packages/angular_devkit/benchmark/README.md @@ -0,0 +1,116 @@ +# Angular Devkit Benchmark + +This tool provides benchmark information for processes. +The tool will gathering metrics from a given command, then average them out over the total runs. + +It currently shows only time, process, cpu, and memory used but might be extended in the future. + +This tool was created to provide an objective and reproducible way of benchmarking process +performance. + +Given a process (or its source code), process inputs and environment, keeping two of these elements +constant while varying the third should allow for meaningful benchmarks over time. + +In the context of the DevKit, we publish many CLI tools and have access to their source code. +By tracking tool resource usage we can catch performance regressions or improvements on our CI. + + +## STABILITY AND SUPPORT DISCLAIMER + +This package is not currently stable. Usage, output and API may change at any time. +Support is not ensured. + +## Installation + +You can install the benchmark tool via `npm install -g benchmark` for a global install, or without +`-g` for a local install. +Installing globally gives you access to the `benchmark` binary in your `PATH`. + + +## CLI Usage + +Call the `benchmark` binary, followed by options, then double dash, then the command to benchmark. + +For more information on the available options, run `benchmark --help`: +``` +$ benchmark --help +[benchmark] benchmark [options] -- [command to benchmark] + +Collects process stats from running the command. + +Options: + --help Show this message. + (... other available options) + +Example: + benchmark --iterations=3 -- node my-script.js +``` + + +## Example + +Given the naive implementation of a fibonacci number calculator below: +``` +// fibonacci.js +const fib = (n) => n > 1 ? fib(n - 1) + fib(n - 2) : n; +console.log(fib(parseInt(process.argv[2]))); +``` + +Run `benchmark -- node fibonacci.js 40` to benchmark calculating the 40th fibonacci number: + +``` +$ benchmark -- node fibonacci.js 40 +[benchmark] Benchmarking process over 5 iterations, with up to 5 retries. +[benchmark] node fibonacci.js 40 (at D:\sandbox\latest-project) +[benchmark] Process Stats +[benchmark] Elapsed Time: 2365.40 ms (2449.00, 2444.00, 2357.00, 2312.00, 2265.00) +[benchmark] Average Process usage: 1.00 process(es) (1.00, 1.00, 1.00, 1.00, 1.00) +[benchmark] Peak Process usage: 1.00 process(es) (1.00, 1.00, 1.00, 1.00, 1.00) +[benchmark] Average CPU usage: 4.72 % (5.03, 4.86, 4.50, 4.50, 4.69) +[benchmark] Peak CPU usage: 23.40 % (25.00, 23.40, 21.80, 21.80, 25.00) +[benchmark] Average Memory usage: 22.34 MB (22.32, 22.34, 22.34, 22.35, 22.35) +[benchmark] Peak Memory usage: 22.34 MB (22.32, 22.34, 22.34, 22.35, 22.35) +``` + + +## API Usage + +You can also use the benchmarking API directly: + +``` +import { Command, defaultStatsCapture, runBenchmark } from '@angular-devkit/benchmark'; + +const command = new Command('node', ['fibonacci.js', '40']); +const captures = [defaultStatsCapture]; + +runBenchmark({ command, command }).subscribe(results => { + // results is:[{ + // "name": "Process Stats", + // "metrics": [{ + // "name": "Elapsed Time", "unit": "ms", "value": 1883.6, + // "componentValues": [1733, 1957, 1580, 1763, 2385] + // }, { + // "name": "Average Process usage", "unit": "process(es)", "value": 1, + // "componentValues": [1, 1, 1, 1, 1] + // }, { + // "name": "Peak Process usage", "unit": "process(es)", "value": 1, + // "componentValues": [1, 1, 1, 1, 1] + // }, { + // "name": "Average CPU usage", "unit": "%", "value": 3.0855555555555556, + // "componentValues": [1.9625, 1.9500000000000002, 1.9500000000000002, 4.887499999999999, 4.677777777777778] + // }, { + // "name": "Peak CPU usage", "unit": "%", "value": 19.380000000000003, + // "componentValues": [15.7, 15.6, 15.6, 25, 25] + // }, { + // "name": "Average Memory usage", "unit": "MB", "value": 22.364057600000002, + // "componentValues": [22.383104, 22.332416, 22.401024, 22.355968, 22.347776] + // }, { + // "name": "Peak Memory usage", "unit": "MB", "value": 22.3649792, + // "componentValues": [22.384639999999997, 22.335487999999998, 22.401024, 22.355968, 22.347776] + // }] + // }] +}); +``` + +A good example of API usage is the `main` binary itself, found in `./src/main.ts`. +We recommend using TypeScript to get full access to the interfaces included. \ No newline at end of file diff --git a/packages/angular_devkit/benchmark/package.json b/packages/angular_devkit/benchmark/package.json new file mode 100644 index 000000000000..a65131ac623b --- /dev/null +++ b/packages/angular_devkit/benchmark/package.json @@ -0,0 +1,24 @@ +{ + "name": "@angular-devkit/benchmark", + "version": "0.0.0", + "private": true, + "description": "Angular Benchmark", + "bin": { + "benchmark": "./src/main.js" + }, + "keywords": [ + "benchmark" + ], + "engines": { + "node": ">= 8.9.0", + "npm": ">= 5.5.1" + }, + "dependencies": { + "@angular-devkit/core": "0.0.0", + "minimist": "^1.2.0", + "pidusage": "^2.0.16", + "pidtree": "^0.3.0", + "rxjs": "~6.2.0", + "tree-kill": "^1.2.0" + } +} \ No newline at end of file diff --git a/packages/angular_devkit/benchmark/src/command.ts b/packages/angular_devkit/benchmark/src/command.ts new file mode 100644 index 000000000000..03003a0f11b1 --- /dev/null +++ b/packages/angular_devkit/benchmark/src/command.ts @@ -0,0 +1,23 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export class Command { + constructor( + public cmd: string, + public args: string[] = [], + public cwd: string = process.cwd(), + public expectedExitCode = 0, + ) { } + + toString() { + const { cmd, args, cwd } = this; + const argsStr = args.length > 0 ? ' ' + args.join(' ') : ''; + + return `${cmd}${argsStr} (at ${cwd})`; + } +} diff --git a/packages/angular_devkit/benchmark/src/default-reporter.ts b/packages/angular_devkit/benchmark/src/default-reporter.ts new file mode 100644 index 000000000000..9224cbe8c057 --- /dev/null +++ b/packages/angular_devkit/benchmark/src/default-reporter.ts @@ -0,0 +1,24 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { logging, tags } from '@angular-devkit/core'; +import { AggregatedMetric, BenchmarkReporter, Metric } from './interfaces'; + +export const defaultReporter = (logger: logging.Logger): BenchmarkReporter => (process, groups) => { + const toplevelLogger = logger; + const indentLogger = new logging.IndentLogger('benchmark-indent-logger', toplevelLogger); + + const formatMetric = (metric: Metric | AggregatedMetric) => tags.oneLine` + ${metric.name}: ${metric.value.toFixed(2)} ${metric.unit} + ${metric.componentValues ? `(${metric.componentValues.map(v => v.toFixed(2)).join(', ')})` : ''} + `; + + groups.forEach(group => { + toplevelLogger.info(`${group.name}`); + group.metrics.forEach(metric => indentLogger.info(formatMetric(metric))); + }); +}; diff --git a/packages/angular_devkit/benchmark/src/default-stats-capture.ts b/packages/angular_devkit/benchmark/src/default-stats-capture.ts new file mode 100644 index 000000000000..ad3bed3b9252 --- /dev/null +++ b/packages/angular_devkit/benchmark/src/default-stats-capture.ts @@ -0,0 +1,60 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { Observable } from 'rxjs'; +import { map, reduce } from 'rxjs/operators'; +import { AggregatedProcessStats, Capture, MetricGroup, MonitoredProcess } from './interfaces'; +import { cumulativeMovingAverage, max } from './utils'; + + +export const defaultStatsCapture: Capture = ( + process: MonitoredProcess, +): Observable => { + type Accumulator = { + elapsed: number, + avgProcesses: number, + peakProcesses: number, + avgCpu: number, + peakCpu: number, + avgMemory: number, + peakMemory: number, + }; + const seed: Accumulator = { + elapsed: 0, + avgProcesses: 0, + peakProcesses: 0, + avgCpu: 0, + peakCpu: 0, + avgMemory: 0, + peakMemory: 0, + }; + + return process.stats$.pipe( + reduce((acc, val, idx) => ({ + elapsed: val.elapsed, + avgProcesses: cumulativeMovingAverage(acc.avgProcesses, val.processes, idx), + peakProcesses: max(acc.peakProcesses, val.processes), + avgCpu: cumulativeMovingAverage(acc.avgCpu, val.cpu, idx), + peakCpu: max(acc.peakCpu, val.cpu), + avgMemory: cumulativeMovingAverage(acc.avgMemory, val.memory, idx), + peakMemory: max(acc.peakMemory, val.memory), + }), seed), + map(metrics => ({ + name: 'Process Stats', + metrics: [ + { name: 'Elapsed Time', unit: 'ms', value: metrics.elapsed }, + { name: 'Average Process usage', unit: 'process(es)', value: metrics.avgProcesses }, + { name: 'Peak Process usage', unit: 'process(es)', value: metrics.peakProcesses }, + { name: 'Average CPU usage', unit: '%', value: metrics.avgCpu }, + { name: 'Peak CPU usage', unit: '%', value: metrics.peakCpu }, + { name: 'Average Memory usage', unit: 'MB', value: metrics.avgMemory * 1e-6 }, + { name: 'Peak Memory usage', unit: 'MB', value: metrics.peakMemory * 1e-6 }, + ], + })), + ); +}; diff --git a/packages/angular_devkit/benchmark/src/default-stats-capture_spec.ts b/packages/angular_devkit/benchmark/src/default-stats-capture_spec.ts new file mode 100644 index 000000000000..ad367162b3d1 --- /dev/null +++ b/packages/angular_devkit/benchmark/src/default-stats-capture_spec.ts @@ -0,0 +1,48 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { Observable } from 'rxjs'; +import { toArray } from 'rxjs/operators'; +import { defaultStatsCapture } from './default-stats-capture'; +import { AggregatedProcessStats, MonitoredProcess } from './interfaces'; + + +describe('defaultStatsCapture', () => { + it('works', async () => { + const stats$ = new Observable(obs => { + const ignoredStats = { ppid: 1, pid: 1, ctime: 1, timestamp: 1 }; + obs.next({ + processes: 1, cpu: 0, memory: 10 * 1e6, elapsed: 1000, + ...ignoredStats, + }); + obs.next({ + processes: 3, cpu: 40, memory: 2 * 1e6, elapsed: 2000, + ...ignoredStats, + }); + obs.next({ + processes: 5, cpu: 20, memory: 3 * 1e6, elapsed: 3000, + ...ignoredStats, + }); + obs.complete(); + }); + const process = { stats$ } as {} as MonitoredProcess; + + const res = await defaultStatsCapture(process).pipe(toArray()).toPromise(); + expect(res).toEqual([{ + name: 'Process Stats', + metrics: [ + { name: 'Elapsed Time', unit: 'ms', value: 3000 }, + { name: 'Average Process usage', unit: 'process(es)', value: 3 }, + { name: 'Peak Process usage', unit: 'process(es)', value: 5 }, + { name: 'Average CPU usage', unit: '%', value: 20 }, + { name: 'Peak CPU usage', unit: '%', value: 40 }, + { name: 'Average Memory usage', unit: 'MB', value: 5 }, + { name: 'Peak Memory usage', unit: 'MB', value: 10 }, + ], + }]); + }); +}); diff --git a/packages/angular_devkit/benchmark/src/index.ts b/packages/angular_devkit/benchmark/src/index.ts new file mode 100644 index 000000000000..65855d04c410 --- /dev/null +++ b/packages/angular_devkit/benchmark/src/index.ts @@ -0,0 +1,16 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export * from './interfaces'; +export * from './command'; +export * from './default-reporter'; +export * from './default-stats-capture'; +export * from './monitored-process'; +export * from './run-benchmark'; +export * from './utils'; +export * from './main'; diff --git a/packages/angular_devkit/benchmark/src/interfaces.d.ts b/packages/angular_devkit/benchmark/src/interfaces.d.ts new file mode 100644 index 000000000000..215164799ac4 --- /dev/null +++ b/packages/angular_devkit/benchmark/src/interfaces.d.ts @@ -0,0 +1,49 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { Observable } from 'rxjs'; +import { Command } from './command'; + +export interface AggregatedProcessStats { + processes: number; // number of processes + cpu: number; // percentage (from 0 to 100*vcore) + memory: number; // bytes + ppid: number; // PPID + pid: number; // PID + ctime: number; // ms user + system time + elapsed: number; // ms since the start of the process + timestamp: number; // ms since epoch +} + +export interface MonitoredProcess { + stats$: Observable; + stdout$: Observable; + stderr$: Observable; + run(): Observable; + toString(): string; +} + +export interface Metric { + name: string; + unit: string; + value: number; + componentValues?: number[]; +} + +export interface AggregatedMetric extends Metric { + componentValues: number[]; +} + +export interface MetricGroup { + name: string; + metrics: (Metric | AggregatedMetric)[]; +} + +export type Capture = (process: MonitoredProcess) => Observable; + +// TODO: might need to allow reporters to say they are finished. +export type BenchmarkReporter = (command: Command, groups: MetricGroup[]) => void; diff --git a/packages/angular_devkit/benchmark/src/main.ts b/packages/angular_devkit/benchmark/src/main.ts new file mode 100644 index 000000000000..edd39881b35e --- /dev/null +++ b/packages/angular_devkit/benchmark/src/main.ts @@ -0,0 +1,184 @@ +#!/usr/bin/env node +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { logging, tags, terminal } from '@angular-devkit/core'; +import { appendFileSync, writeFileSync } from 'fs'; +import * as minimist from 'minimist'; +import { filter, map, toArray } from 'rxjs/operators'; +import { Command } from '../src/command'; +import { defaultReporter } from '../src/default-reporter'; +import { defaultStatsCapture } from '../src/default-stats-capture'; +import { runBenchmark } from '../src/run-benchmark'; + + +export interface MainOptions { + args: string[]; + stdout?: { write(buffer: string | Buffer): boolean }; + stderr?: { write(buffer: string | Buffer): boolean }; +} + +export async function main({ + args, + stdout = process.stdout, + stderr = process.stderr, +}: MainOptions): Promise<0 | 1> { + + // Show usage of the CLI tool, and exit the process. + function usage(logger: logging.Logger) { + logger.info(tags.stripIndent` + benchmark [options] -- [command to benchmark] + + Collects process stats from running the command. + + Options: + --help Show this message. + --verbose Show more information while running. + --exit-code Expected exit code for the command. Default is 0. + --iterations Number of iterations to run the benchmark over. Default is 5. + --retries Number of times to retry when process fails. Default is 5. + --cwd Current working directory to run the process in. + --output-file File to output benchmark log to. + --overwrite-output-file If the output file should be overwritten rather than appended to. + --prefix Logging prefix. + + Example: + benchmark --iterations=3 -- node my-script.js + `); + } + + interface BenchmarkCliArgv { + help: boolean; + verbose: boolean; + 'overwrite-output-file': boolean; + 'exit-code': number; + iterations: number; + retries: number; + 'output-file': string | null; + cwd: string; + prefix: string; + '--': string[] | null; + } + + // Parse the command line. + const argv = minimist(args, { + boolean: ['help', 'verbose', 'overwrite-output-file'], + default: { + 'exit-code': 0, + 'iterations': 5, + 'retries': 5, + 'output-file': null, + 'cwd': process.cwd(), + 'prefix': '[benchmark]', + }, + '--': true, + }) as {} as BenchmarkCliArgv; + + // Create the DevKit Logger used through the CLI. + const logger = new logging.TransformLogger( + 'benchmark-prefix-logger', + stream => stream.pipe(map(entry => { + if (argv['prefix']) { entry.message = `${argv['prefix']} ${entry.message}`; } + + return entry; + })), + ); + + // Log to console. + logger + .pipe(filter(entry => (entry.level != 'debug' || argv['verbose']))) + .subscribe(entry => { + let color: (s: string) => string = x => terminal.dim(terminal.white(x)); + let output = stdout; + switch (entry.level) { + case 'info': + color = terminal.white; + break; + case 'warn': + color = terminal.yellow; + break; + case 'error': + color = terminal.red; + output = stderr; + break; + case 'fatal': + color = (x: string) => terminal.bold(terminal.red(x)); + output = stderr; + break; + } + + output.write(color(entry.message) + '\n'); + }); + + + // Print help. + if (argv['help']) { + usage(logger); + + return 0; + } + + const commandArgv = argv['--']; + + // Exit early if we can't find the command to benchmark. + if (!commandArgv || !Array.isArray(argv['--']) || (argv['--'] as Array).length < 1) { + logger.fatal(`Missing command, see benchmark --help for help.`); + + return 1; + } + + // Setup file logging. + if (argv['output-file'] !== null) { + if (argv['overwrite-output-file']) { + writeFileSync(argv['output-file'] as string, ''); + } + logger.pipe(filter(entry => (entry.level != 'debug' || argv['verbose']))) + .subscribe(entry => appendFileSync(argv['output-file'] as string, `${entry.message}\n`)); + } + + // Run benchmark on given command, capturing stats and reporting them. + const exitCode = argv['exit-code']; + const cmd = commandArgv[0]; + const cmdArgs = commandArgv.slice(1); + const command = new Command(cmd, cmdArgs, argv['cwd'], exitCode); + const captures = [defaultStatsCapture]; + const reporters = [defaultReporter(logger)]; + const iterations = argv['iterations']; + const retries = argv['retries']; + + logger.info(`Benchmarking process over ${iterations} iterations, with up to ${retries} retries.`); + logger.info(` ${command.toString()}`); + + let res; + try { + res = await runBenchmark( + { command, captures, reporters, iterations, retries, logger }, + ).pipe(toArray()).toPromise(); + } catch (error) { + if (error.message) { + logger.fatal(error.message); + } else { + logger.fatal(error); + } + + return 1; + } + + if (res.length === 0) { + return 1; + } + + return 0; +} + +if (require.main === module) { + const args = process.argv.slice(2); + main({ args }) + .then(exitCode => process.exitCode = exitCode) + .catch(e => { throw (e); }); +} diff --git a/packages/angular_devkit/benchmark/src/main_spec.ts b/packages/angular_devkit/benchmark/src/main_spec.ts new file mode 100644 index 000000000000..eb26a29f2b32 --- /dev/null +++ b/packages/angular_devkit/benchmark/src/main_spec.ts @@ -0,0 +1,145 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs'; +import { basename, dirname, join } from 'path'; +import { main } from './main'; +// tslint:disable-next-line:no-implicit-dependencies +const temp = require('temp'); + + +// We only care about the write method in these mocks of NodeJS.WriteStream. +class MockWriteStream { + lines: string[] = []; + write(str: string) { + // Strip color control characters. + this.lines.push(str.replace(/[^\x20-\x7F]\[\d+m/g, '')); + + return true; + } +} + +describe('benchmark binary', () => { + const benchmarkScript = require.resolve(join(__dirname, './test/fibonacci.js')); + const exitCodeOneScript = require.resolve(join(__dirname, './test/exit-code-one.js')); + const outputFileRoot = temp.mkdirSync('benchmark-binary-spec-'); + const outputFile = join(outputFileRoot, 'output.log'); + let stdout: MockWriteStream, stderr: MockWriteStream; + + beforeEach(() => { + stdout = new MockWriteStream(); + stderr = new MockWriteStream(); + }); + + afterEach(() => { + if (existsSync(outputFile)) { unlinkSync(outputFile); } + }); + + it('works', async () => { + const args = ['--', 'node', benchmarkScript, '30']; + const res = await main({ args, stdout, stderr }); + expect(stdout.lines).toContain('[benchmark] Process Stats\n'); + expect(res).toEqual(0); + }); + + it('fails with no command', async () => { + const args: string[] = []; + const res = await main({ args, stdout, stderr }); + expect(stderr.lines).toContain( + '[benchmark] Missing command, see benchmark --help for help.\n'); + expect(res).toEqual(1); + }); + + it('fails with when exit code is not expected', async () => { + const args: string[] = ['--', 'node', exitCodeOneScript]; + const res = await main({ args, stdout, stderr }); + expect(stderr.lines).toContain( + '[benchmark] Maximum number of retries (5) for command was exceeded.\n'); + expect(res).toEqual(1); + }); + + it('prints help', async () => { + const args = ['--help']; + const res = await main({ args, stdout, stderr }); + // help is a multiline write. + expect(stdout.lines[0]).toContain('Options:\n'); + expect(res).toEqual(0); + }); + + it('uses verbose', async () => { + const args = ['--verbose', '--', 'node', benchmarkScript, '30']; + const res = await main({ args, stdout, stderr }); + expect(stdout.lines).toContain('[benchmark] Run #1: finished successfully\n'); + expect(res).toEqual(0); + }); + + it('uses exit code', async () => { + const args = ['--exit-code', '1', '--', 'node', exitCodeOneScript]; + const res = await main({ args, stdout, stderr }); + expect(stdout.lines).toContain('[benchmark] Process Stats\n'); + expect(res).toEqual(0); + }); + + it('uses iterations', async () => { + const args = ['--iterations', '3', '--', 'node', benchmarkScript, '30']; + const res = await main({ args, stdout, stderr }); + expect(stdout.lines).toContain( + '[benchmark] Benchmarking process over 3 iterations, with up to 5 retries.\n'); + expect(res).toEqual(0); + }); + + it('uses retries', async () => { + const args = ['--retries', '3', '--', 'node', benchmarkScript, '30']; + const res = await main({ args, stdout, stderr }); + expect(stdout.lines).toContain( + '[benchmark] Benchmarking process over 5 iterations, with up to 3 retries.\n'); + expect(res).toEqual(0); + }); + + it('uses cwd', async () => { + const args = ['--cwd', dirname(benchmarkScript), '--', 'node', basename(benchmarkScript), '30']; + const res = await main({ args, stdout, stderr }); + expect(stdout.lines).toContain('[benchmark] Process Stats\n'); + expect(res).toEqual(0); + }); + + it('uses output-file', async () => { + const args = ['--output-file', outputFile, '--', 'node', benchmarkScript, '30']; + const res = await main({ args, stdout, stderr }); + expect(res).toEqual(0); + expect(existsSync(outputFile)).toBe(true, 'outputFile exists'); + expect(readFileSync(outputFile, 'utf-8')).toContain('[benchmark] Process Stats'); + }); + + it('appends to output-file', async () => { + writeFileSync(outputFile, 'existing line'); + const args = ['--output-file', outputFile, '--', 'node', benchmarkScript, '30']; + const res = await main({ args, stdout, stderr }); + expect(res).toEqual(0); + expect(existsSync(outputFile)).toBe(true, 'outputFile exists'); + expect(readFileSync(outputFile, 'utf-8')).toContain('existing line'); + }); + + it('overwrites output-file', async () => { + writeFileSync(outputFile, 'existing line'); + const args = [ + '--output-file', outputFile, '--overwrite-output-file', + '--', 'node', benchmarkScript, '30', + ]; + const res = await main({ args, stdout, stderr }); + expect(res).toEqual(0); + expect(existsSync(outputFile)).toBe(true, 'outputFile exists'); + expect(readFileSync(outputFile, 'utf-8')).not.toContain('existing line'); + }); + + it('uses prefix', async () => { + const args = ['--prefix', '[abc]', '--', 'node', benchmarkScript, '30']; + const res = await main({ args, stdout, stderr }); + stdout.lines.forEach(line => expect(line).toMatch(/^\[abc\]/)); + expect(res).toEqual(0); + }); +}); diff --git a/packages/angular_devkit/benchmark/src/monitored-process.ts b/packages/angular_devkit/benchmark/src/monitored-process.ts new file mode 100644 index 000000000000..885736121e43 --- /dev/null +++ b/packages/angular_devkit/benchmark/src/monitored-process.ts @@ -0,0 +1,106 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { SpawnOptions, spawn } from 'child_process'; +import { Observable, Subject, from, timer } from 'rxjs'; +import { concatMap, map, onErrorResumeNext, tap } from 'rxjs/operators'; +import { Command } from './command'; +import { AggregatedProcessStats, MonitoredProcess } from './interfaces'; +const pidusage = require('pidusage'); +const pidtree = require('pidtree'); +const treeKill = require('tree-kill'); + + +// Cleanup when the parent process exits. +const defaultProcessExitCb = () => { }; +let processExitCb = defaultProcessExitCb; +process.on('exit', () => { + processExitCb(); + processExitCb = defaultProcessExitCb; +}); + +export class LocalMonitoredProcess implements MonitoredProcess { + private stats = new Subject(); + private stdout = new Subject(); + private stderr = new Subject(); + private pollingRate = 100; + stats$: Observable = this.stats.asObservable(); + stdout$: Observable = this.stdout.asObservable(); + stderr$: Observable = this.stderr.asObservable(); + + constructor(private command: Command) { } + + run(): Observable { + return new Observable(obs => { + const { cmd, cwd, args } = this.command; + const spawnOptions: SpawnOptions = { cwd: cwd, shell: true }; + + // Spawn the process. + const childProcess = spawn(cmd, args, spawnOptions); + + // Emit output and stats. + childProcess.stdout.on('data', (data: Buffer) => this.stdout.next(data)); + childProcess.stderr.on('data', (data: Buffer) => this.stderr.next(data)); + const statsSubs = timer(0, this.pollingRate).pipe( + concatMap(() => from(pidtree(childProcess.pid, { root: true }))), + concatMap((pids: number[]) => from(pidusage(pids, { maxage: 5 * this.pollingRate }))), + map((statsByProcess: { [key: string]: AggregatedProcessStats }) => { + // Ignore the spawned shell in the total process number. + const pids = Object.keys(statsByProcess) + .filter(pid => pid != childProcess.pid.toString()); + const processes = pids.length; + // We want most stats from the parent process. + const { pid, ppid, ctime, elapsed, timestamp } = statsByProcess[childProcess.pid]; + // CPU and memory should be agreggated. + let cpu = 0, memory = 0; + for (const pid of pids) { + cpu += statsByProcess[pid].cpu; + memory += statsByProcess[pid].memory; + } + + return { + processes, cpu, memory, pid, ppid, ctime, elapsed, timestamp, + } as AggregatedProcessStats; + }), + tap(stats => this.stats.next(stats)), + onErrorResumeNext(), + ).subscribe(); + + // Process event handling. + + // Killing processes cross platform can be hard, treeKill helps. + const killChildProcess = () => { + if (childProcess && childProcess.pid) { + treeKill(childProcess.pid, 'SIGTERM'); + } + }; + + // Convert process exit codes and errors into observable events. + const handleChildProcessExit = (code?: number, error?: Error) => { + // Stop gathering stats and complete subjects. + statsSubs.unsubscribe(); + this.stats.complete(); + this.stdout.complete(); + this.stderr.complete(); + + // Kill hanging child processes and emit error/exit code. + killChildProcess(); + if (error) { + obs.error(error); + } + obs.next(code); + obs.complete(); + }; + childProcess.once('exit', handleChildProcessExit); + childProcess.once('error', (err) => handleChildProcessExit(1, err)); + processExitCb = killChildProcess; + + // Cleanup on unsubscription. + return () => childProcess.kill(); + }); + } +} diff --git a/packages/angular_devkit/benchmark/src/monitored-process_spec.ts b/packages/angular_devkit/benchmark/src/monitored-process_spec.ts new file mode 100644 index 000000000000..12757d6e8dac --- /dev/null +++ b/packages/angular_devkit/benchmark/src/monitored-process_spec.ts @@ -0,0 +1,50 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { dirname } from 'path'; +import { toArray } from 'rxjs/operators'; +import { Command } from './command'; +import { LocalMonitoredProcess } from './monitored-process'; + +describe('LocalMonitoredProcess', () => { + const cmd = new Command( + 'node', + ['test-script.js'], + dirname(require.resolve('./test/test-script.js')), + ); + + it('works', async () => { + const process = new LocalMonitoredProcess(cmd); + const res = await process.run().pipe(toArray()).toPromise(); + expect(res).toEqual([0]); + }); + + it('captures stdout', async () => { + const process = new LocalMonitoredProcess(cmd); + const stdoutOutput: string[] = []; + process.stdout$.subscribe(data => stdoutOutput.push(data.toString())); + await process.run().pipe().toPromise(); + expect(stdoutOutput).toEqual(['stdout start\n', 'stdout end\n']); + }); + + it('captures stderr', async () => { + const process = new LocalMonitoredProcess(cmd); + const stdoutOutput: string[] = []; + process.stderr$.subscribe(data => stdoutOutput.push(data.toString())); + await process.run().pipe().toPromise(); + expect(stdoutOutput).toEqual(['stderr start\n', 'stderr end\n']); + }); + + it('captures stats', async () => { + const process = new LocalMonitoredProcess(cmd); + const statsOutput: string[] = []; + process.stderr$.subscribe(data => statsOutput.push(data.toString())); + await process.run().pipe().toPromise(); + expect(statsOutput.length).toBeGreaterThan(0); + }); +}); diff --git a/packages/angular_devkit/benchmark/src/run-benchmark.ts b/packages/angular_devkit/benchmark/src/run-benchmark.ts new file mode 100644 index 000000000000..2cee80479a7b --- /dev/null +++ b/packages/angular_devkit/benchmark/src/run-benchmark.ts @@ -0,0 +1,98 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { BaseException, logging } from '@angular-devkit/core'; +import { Observable, forkJoin, of, throwError } from 'rxjs'; +import { concatMap, map, retryWhen, take, tap, throwIfEmpty } from 'rxjs/operators'; +import { Command } from './command'; +import { BenchmarkReporter, Capture, MetricGroup } from './interfaces'; +import { LocalMonitoredProcess } from './monitored-process'; +import { aggregateMetricGroups } from './utils'; + +export interface RunBenchmarkOptions { + command: Command; + captures: Capture[]; + reporters: BenchmarkReporter[]; + iterations?: number; + retries?: number; + expectedExitCode?: number; + logger?: logging.Logger; +} + +export class MaximumRetriesExceeded extends BaseException { + constructor(retries: number) { + super(`Maximum number of retries (${retries}) for command was exceeded.`); + } +} + +export function runBenchmark({ + command, captures, reporters = [], iterations = 5, retries = 5, logger = new logging.NullLogger(), +}: RunBenchmarkOptions): Observable { + + let successfulRuns = 0; + let failedRuns = 0; + const notDoneYet = new BaseException('Not done yet.'); + const processFailed = new BaseException('Wrong exit code.'); + const debugPrefix = () => `Run #${successfulRuns + 1}:`; + let aggregatedMetricGroups: MetricGroup[] = []; + + // Run the process and captures, wait for both to finish, and average out the metrics. + return new Observable(obs => { + const monitoredProcess = new LocalMonitoredProcess(command); + const metric$ = captures.map(capture => capture(monitoredProcess)); + obs.next([monitoredProcess, ...metric$]); + }).pipe( + tap(() => logger.debug(`${debugPrefix()} starting`)), + concatMap(([monitoredProcess, ...metric$]) => forkJoin(monitoredProcess.run(), ...metric$)), + throwIfEmpty(() => new Error('Nothing was captured')), + concatMap((results) => { + const [processExitCode, ...metrics] = results; + + if ((processExitCode as number) != command.expectedExitCode) { + logger.debug(`${debugPrefix()} exited with ${processExitCode} but ` + + `${command.expectedExitCode} was expected`); + + return throwError(processFailed); + } + + logger.debug(`${debugPrefix()} finished successfully`); + + return of(metrics as MetricGroup[]); + }), + map(newMetricGroups => { + // Aggregate metric groups into a single one. + if (aggregatedMetricGroups.length === 0) { + aggregatedMetricGroups = newMetricGroups; + } else { + aggregatedMetricGroups = aggregatedMetricGroups.map((_, idx) => + aggregateMetricGroups(aggregatedMetricGroups[idx], newMetricGroups[idx]), + ); + } + + successfulRuns += 1; + + return aggregatedMetricGroups; + }), + concatMap(val => successfulRuns < iterations ? throwError(notDoneYet) : of(val)), + // This is where we control when the process should be run again. + retryWhen(errors => errors.pipe(concatMap(val => { + // Always run again while we are not done yet. + if (val === notDoneYet) { return of(val); } + + // Otherwise check if we're still within the retry threshold. + failedRuns += 1; + if (failedRuns < retries) { return of(val); } + + if (val === processFailed) { return throwError(new MaximumRetriesExceeded(retries)); } + + // Not really sure what happened here, just re-throw it. + return throwError(val); + }))), + tap(groups => reporters.forEach(reporter => reporter(command, groups))), + take(1), + ); +} diff --git a/packages/angular_devkit/benchmark/src/test/exit-code-one.js b/packages/angular_devkit/benchmark/src/test/exit-code-one.js new file mode 100644 index 000000000000..7b777488a01b --- /dev/null +++ b/packages/angular_devkit/benchmark/src/test/exit-code-one.js @@ -0,0 +1 @@ +process.exit(1); \ No newline at end of file diff --git a/packages/angular_devkit/benchmark/src/test/fibonacci.js b/packages/angular_devkit/benchmark/src/test/fibonacci.js new file mode 100644 index 000000000000..58aafbc51023 --- /dev/null +++ b/packages/angular_devkit/benchmark/src/test/fibonacci.js @@ -0,0 +1,2 @@ +const fib = (n) => n > 1 ? fib(n - 1) + fib(n - 2) : n; +console.log(fib(parseInt(process.argv[2]))); \ No newline at end of file diff --git a/packages/angular_devkit/benchmark/src/test/test-script.js b/packages/angular_devkit/benchmark/src/test/test-script.js new file mode 100644 index 000000000000..a9ad5213158e --- /dev/null +++ b/packages/angular_devkit/benchmark/src/test/test-script.js @@ -0,0 +1,7 @@ +console.log("stdout start"); +console.error("stderr start"); + +setTimeout(() => { + console.log("stdout end"); + console.error("stderr end"); +}, 1000); diff --git a/packages/angular_devkit/benchmark/src/utils.ts b/packages/angular_devkit/benchmark/src/utils.ts new file mode 100644 index 000000000000..ea8fc36205ee --- /dev/null +++ b/packages/angular_devkit/benchmark/src/utils.ts @@ -0,0 +1,49 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { AggregatedMetric, Metric, MetricGroup } from './interfaces'; + +// Prefers to keep v1 when both are equal. +export const max = (v1: number, v2: number) => v2 > v1 ? v2 : v1; + +export const cumulativeMovingAverage = (acc: number, val: number, accSize: number) => + (val + accSize * acc) / (accSize + 1); + +export const aggregateMetrics = ( + m1: Metric | AggregatedMetric, + m2: Metric | AggregatedMetric, +): AggregatedMetric => { + if ((m1.name != m2.name) || (m1.unit != m2.unit)) { + throw new Error('Cannot aggregate metrics with different names or units:'); + } + + const m1Values = m1.componentValues ? m1.componentValues : [m1.value]; + const m2Values = m2.componentValues ? m2.componentValues : [m2.value]; + + return { + name: m1.name, + unit: m1.unit, + // m1.value already holds an average if it has component values. + value: m2Values.reduce( + (acc, val, idx) => cumulativeMovingAverage(acc, val, idx + m1Values.length), + m1.value, + ), + componentValues: [...m1Values, ...m2Values], + }; +}; + +export const aggregateMetricGroups = (g1: MetricGroup, g2: MetricGroup): MetricGroup => { + if (g1.name != g2.name || g1.metrics.length != g2.metrics.length) { + throw new Error('Cannot aggregate metric groups with different names.'); + } + + return { + name: g1.name, + metrics: g1.metrics.map((_, idx) => aggregateMetrics(g1.metrics[idx], g2.metrics[idx])), + }; +}; diff --git a/yarn.lock b/yarn.lock index f39320644e77..a90fc11356d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5719,6 +5719,16 @@ performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" +pidtree@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.3.0.tgz#f6fada10fccc9f99bf50e90d0b23d72c9ebc2e6b" + +pidusage@^2.0.16: + version "2.0.16" + resolved "https://registry.yarnpkg.com/pidusage/-/pidusage-2.0.16.tgz#46e2e3185eaef253ef6303f766f7aae72f74f98c" + dependencies: + safe-buffer "^5.1.2" + pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" From 1df6126f03fa222f7c0cf351ea8ed0c825cfd435 Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Wed, 29 Aug 2018 15:22:48 +0100 Subject: [PATCH 2/4] test: add AIO benchmark --- benchmark/aio/.gitignore | 1 + benchmark/aio/package.json | 17 +++++++++++++++++ tsconfig.json | 3 ++- 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 benchmark/aio/.gitignore create mode 100644 benchmark/aio/package.json diff --git a/benchmark/aio/.gitignore b/benchmark/aio/.gitignore new file mode 100644 index 000000000000..3c85010e11ea --- /dev/null +++ b/benchmark/aio/.gitignore @@ -0,0 +1 @@ +angular/ \ No newline at end of file diff --git a/benchmark/aio/package.json b/benchmark/aio/package.json new file mode 100644 index 000000000000..37cdfa474b9a --- /dev/null +++ b/benchmark/aio/package.json @@ -0,0 +1,17 @@ +{ + "name": "aio-benchmark", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "initialize": "yarn clone && yarn setup && yarn update", + "clone": "(git clone https://github.com/angular/angular --depth 1 || true) && cd angular && git fetch origin dd2a650c3455f3bc0a88f8181758a84aacb25fea && git checkout -f FETCH_HEAD", + "setup": "cd angular && yarn && cd aio && yarn && yarn setup", + "//": "Shouldn't need to install the package twice, but the first install seems to leave two @ngtools/webpack installs around.", + "update": "cd angular/aio && yarn add ../../../../dist/@angular-devkit_build-angular.tgz --dev && yarn add ../../../../dist/_angular-devkit_build-angular.tgz --dev", + "benchmark": "cd angular/aio && benchmark --verbose -- yarn ~~build --configuration=stable" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/tsconfig.json b/tsconfig.json index ac88e47f5fad..c4a593422654 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -64,6 +64,7 @@ "packages/schematics/*/*/*files/**/*", "tmp/**/*", "scripts/patches/**/*", - "tests/**/*" + "tests/**/*", + "benchmark/**/*", ] } From d0d204eb8394e1abdc09758fb99b0ee6d5e3454c Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Fri, 31 Aug 2018 14:09:03 +0100 Subject: [PATCH 3/4] build: fix tarring packages on windows --- scripts/build.ts | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/scripts/build.ts b/scripts/build.ts index 0d89df98f1f8..9c24b8b0e781 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -37,16 +37,48 @@ function _mkdirp(p: string) { fs.mkdirSync(p); } +function _recursiveFileList(p: string): string[] { + if (!fs.statSync(p).isDirectory()) { + return []; + } + + const list = fs.readdirSync(p); + + return list + .map(subpath => { + const subpathList = _recursiveFileList(path.join(p, subpath)); + + return [ subpath, ...subpathList.map(sp => path.join(subpath, sp))]; + }) + // Flatten. + .reduce((acc, curr) => [...acc, ...curr], []) + // Filter out directories. + .filter(sp => !fs.statSync(path.join(p, sp)).isDirectory()); +} + +// This method mimics how npm pack tars packages. function _tar(out: string, dir: string) { + // NOTE: node-tar does some Magic Stuff depending on prefixes for files + // specifically with @ signs, so we just neutralize that one + // and any such future "features" by prepending `./` + + // Without this, the .tar file cannot be opened on Windows. + + const files = _recursiveFileList(dir).map((f) => `./${f}`); + return tar.create({ gzip: true, strict: true, portable: true, cwd: dir, + prefix: 'package/', file: out, sync: true, - }, ['.']); + // Provide a specific date in the 1980s for the benefit of zip, + // which is confounded by files dated at the Unix epoch 0. + mtime: new Date('1985-10-26T08:15:00.000Z'), + }, files); } From a9b1ca1672789c781e42839ab0f70b74bf82df61 Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Fri, 31 Aug 2018 14:11:04 +0100 Subject: [PATCH 4/4] build: workaround yarn tgz bug --- benchmark/aio/package.json | 3 ++- lib/packages.ts | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/benchmark/aio/package.json b/benchmark/aio/package.json index 37cdfa474b9a..fce9b14e36f7 100644 --- a/benchmark/aio/package.json +++ b/benchmark/aio/package.json @@ -7,8 +7,9 @@ "initialize": "yarn clone && yarn setup && yarn update", "clone": "(git clone https://github.com/angular/angular --depth 1 || true) && cd angular && git fetch origin dd2a650c3455f3bc0a88f8181758a84aacb25fea && git checkout -f FETCH_HEAD", "setup": "cd angular && yarn && cd aio && yarn && yarn setup", + "update": "cd angular/aio && yarn add ../../../../dist/_angular-devkit_build-angular.tgz --dev", "//": "Shouldn't need to install the package twice, but the first install seems to leave two @ngtools/webpack installs around.", - "update": "cd angular/aio && yarn add ../../../../dist/@angular-devkit_build-angular.tgz --dev && yarn add ../../../../dist/_angular-devkit_build-angular.tgz --dev", + "postupdate": "cd angular/aio && yarn add ../../../../dist/_angular-devkit_build-angular.tgz --dev", "benchmark": "cd angular/aio && benchmark --verbose -- yarn ~~build --configuration=stable" }, "keywords": [], diff --git a/lib/packages.ts b/lib/packages.ts index d418943173c4..26eaed8dcf0d 100644 --- a/lib/packages.ts +++ b/lib/packages.ts @@ -212,7 +212,9 @@ export const packages: PackageMap = relative: path.relative(path.dirname(__dirname), pkgRoot), main: path.resolve(pkgRoot, 'src/index.ts'), private: packageJson.private, - tar: path.join(distRoot, name.replace('/', '_') + '.tgz'), + // yarn doesn't take kindly to @ in tgz filenames + // https://github.com/yarnpkg/yarn/issues/6339 + tar: path.join(distRoot, name.replace(/\/|@/g, '_') + '.tgz'), bin, name, packageJson,