diff --git a/packages/_/benchmark/src/benchmark.ts b/packages/_/benchmark/src/benchmark.ts index c02fc605ee25..7c22abe0136f 100644 --- a/packages/_/benchmark/src/benchmark.ts +++ b/packages/_/benchmark/src/benchmark.ts @@ -15,11 +15,11 @@ declare const global: { const kNanosecondsPerSeconds = 1e9; const kBenchmarkIterationMaxCount = 10000; const kBenchmarkTimeoutInMsec = 5000; -const kWarmupIterationCount = 10; +const kWarmupIterationCount = 100; const kTopMetricCount = 5; -function _run(fn: () => void, collector: number[]) { +function _run(fn: (i: number) => void, collector: number[]) { const timeout = Date.now(); // Gather the first 5 seconds runs, or kMaxNumberOfIterations runs whichever comes first // (soft timeout). @@ -28,7 +28,7 @@ function _run(fn: () => void, collector: number[]) { i++) { // Start time. const start = process.hrtime(); - fn(); + fn(i); // Get the stop difference time. const diff = process.hrtime(start); @@ -41,13 +41,15 @@ function _run(fn: () => void, collector: number[]) { function _stats(metrics: number[]) { metrics.sort((a, b) => a - b); - const middle = metrics.length / 2; + const count = metrics.length; + const middle = count / 2; const mean = Number.isInteger(middle) ? metrics[middle] : ((metrics[middle - 0.5] + metrics[middle + 0.5]) / 2); const total = metrics.reduce((acc, curr) => acc + curr, 0); - const average = total / metrics.length; + const average = total / count; return { + count: count, fastest: metrics.slice(0, kTopMetricCount), slowest: metrics.reverse().slice(0, kTopMetricCount), mean, @@ -56,12 +58,12 @@ function _stats(metrics: number[]) { } -export function benchmark(name: string, fn: () => void, base?: () => void) { +export function benchmark(name: string, fn: (i: number) => void, base?: (i: number) => void) { it(name + ' (time in nanoseconds)', (done) => { process.nextTick(() => { for (let i = 0; i < kWarmupIterationCount; i++) { // Warm it up. - fn(); + fn(i); } const reporter = global.benchmarkReporter; diff --git a/packages/angular_devkit/core/package.json b/packages/angular_devkit/core/package.json index dd72b800d1db..b6ae6091af4f 100644 --- a/packages/angular_devkit/core/package.json +++ b/packages/angular_devkit/core/package.json @@ -11,7 +11,10 @@ "ajv": "6.5.3", "chokidar": "2.0.4", "fast-json-stable-stringify": "2.0.0", - "source-map": "0.7.3", - "rxjs": "6.3.3" + "rxjs": "6.3.3", + "source-map": "0.7.3" + }, + "devDependencies": { + "seedrandom": "^2.4.4" } } diff --git a/packages/angular_devkit/core/src/virtual-fs/path.ts b/packages/angular_devkit/core/src/virtual-fs/path.ts index 11ec697b0311..0fd5eb374116 100644 --- a/packages/angular_devkit/core/src/virtual-fs/path.ts +++ b/packages/angular_devkit/core/src/virtual-fs/path.ts @@ -185,9 +185,27 @@ export function fragment(path: string): PathFragment { } +/** + * normalize() cache to reduce computation. For now this grows and we never flush it, but in the + * future we might want to add a few cache flush to prevent this from growing too large. + */ +let normalizedCache = new Map(); + + +/** + * Reset the cache. This is only useful for testing. + * @private + */ +export function resetNormalizeCache() { + normalizedCache = new Map(); +} + + /** * Normalize a string into a Path. This is the only mean to get a Path type from a string that - * represents a system path. Normalization includes: + * represents a system path. This method cache the results as real world paths tend to be + * duplicated often. + * Normalization includes: * - Windows backslashes `\\` are replaced with `/`. * - Windows drivers are replaced with `/X/`, where X is the drive letter. * - Absolute paths starts with `/`. @@ -195,8 +213,23 @@ export function fragment(path: string): PathFragment { * - Path segments `.` are removed. * - Path segments `..` are resolved. * - If a path is absolute, having a `..` at the start is invalid (and will throw). + * @param path The path to be normalized. */ export function normalize(path: string): Path { + let maybePath = normalizedCache.get(path); + if (!maybePath) { + maybePath = noCacheNormalize(path); + normalizedCache.set(path, maybePath); + } + + return maybePath; +} + + +/** + * The no cache version of the normalize() function. Used for benchmarking and testing. + */ +export function noCacheNormalize(path: string): Path { if (path == '' || path == '.') { return '' as Path; } else if (path == NormalizedRoot) { diff --git a/packages/angular_devkit/core/src/virtual-fs/path_benchmark.ts b/packages/angular_devkit/core/src/virtual-fs/path_benchmark.ts index a2ab0824a1f4..9973c625bae7 100644 --- a/packages/angular_devkit/core/src/virtual-fs/path_benchmark.ts +++ b/packages/angular_devkit/core/src/virtual-fs/path_benchmark.ts @@ -7,14 +7,82 @@ */ // tslint:disable:no-implicit-dependencies import { benchmark } from '@_/benchmark'; -import { join, normalize } from './path'; +import { join, noCacheNormalize, normalize, resetNormalizeCache } from './path'; + +const seedrandom = require('seedrandom'); const p1 = '/b/././a/tt/../../../a/b/./d/../c'; const p2 = '/a/tt/../../../a/b/./d'; +const numRandomIter = 10000; + + describe('Virtual FS Path', () => { - benchmark('normalize', () => normalize(p1)); - benchmark('join', () => join(normalize(p1), normalize(p2))); + benchmark('join', () => join(normalize(p1), p2)); + + describe('normalize', () => { + let rng: () => number; + let cases: string[]; + + // Math.random() doesn't allow us to set a seed, so we use a library. + beforeEach(() => { + rng = seedrandom('some fixed value'); + + function _str(len: number) { + let r = ''; + const space = 'abcdefghijklmnopqrstuvwxyz0123456789'; + for (let i = 0; i < len; i++) { + r += space[Math.floor(rng() * space.length)]; + } + + return r; + } + + // Build test cases. + cases = new Array(numRandomIter) + .fill(0) + .map(() => { + return new Array(Math.floor(rng() * 20 + 5)) + .fill(0) + .map(() => _str(rng() * 20 + 3)) + .join('/'); + }); + + resetNormalizeCache(); + }); + + describe('random (0 cache hits)', () => { + benchmark('', i => normalize(cases[i]), i => noCacheNormalize(cases[i])); + }); + + describe('random (10% cache hits)', () => { + beforeEach(() => { + cases = cases.map(x => (rng() < 0.1) ? cases[0] : x); + }); + benchmark('', i => normalize(cases[i]), i => noCacheNormalize(cases[i])); + }); + + describe('random (30% cache hits)', () => { + beforeEach(() => { + cases = cases.map(x => (rng() < 0.3) ? cases[0] : x); + }); + benchmark('', i => normalize(cases[i]), i => noCacheNormalize(cases[i])); + }); + + describe('random (50% cache hits)', () => { + beforeEach(() => { + cases = cases.map(x => (rng() < 0.5) ? cases[0] : x); + }); + benchmark('', i => normalize(cases[i]), i => noCacheNormalize(cases[i])); + }); + + describe('random (80% cache hits)', () => { + beforeEach(() => { + cases = cases.map(x => (rng() < 0.8) ? cases[0] : x); + }); + benchmark('', i => normalize(cases[i]), i => noCacheNormalize(cases[i])); + }); + }); }); diff --git a/scripts/benchmark.ts b/scripts/benchmark.ts index 3a19da2d32df..822d798f918d 100644 --- a/scripts/benchmark.ts +++ b/scripts/benchmark.ts @@ -27,6 +27,7 @@ declare const global: { interface BenchmarkResult { + count: number; slowest: number[]; fastest: number[]; mean: number; @@ -71,12 +72,17 @@ class BenchmarkReporter extends JasmineSpecReporter implements jasmine.CustomRep return p.substr(0, p.length - ('' + s).length) + s; } + const count = pad(stat.count); const fastest = stat.fastest.map(x => pad(x)).join(''); const slowest = stat.slowest.map(x => pad(x)).join(''); const mean = pad(Math.floor(stat.mean)); const average = pad(Math.floor(stat.average)); if (stat.base) { - const precision = (x: number) => ('' + Math.floor(x * 100)).replace(/(\d\d)$/, '.$1'); + const precision = (x: number) => { + x = Math.floor(x * 100); + + return `${Math.floor(x / 100)}.${Math.floor(x / 10) % 10}${x % 10}`; + }; const multPad = ' '; const baseFastest = stat.base.fastest.map(x => pad(x)).join(''); const baseSlowest = stat.base.slowest.map(x => pad(x)).join(''); @@ -86,6 +92,7 @@ class BenchmarkReporter extends JasmineSpecReporter implements jasmine.CustomRep const baseAverageMult = pad(precision(stat.average / stat.base.average), multPad); console.log(terminal.colors.yellow(tags.indentBy(6)` + count: ${count} fastest: ${fastest} (base) ${baseFastest} slowest: ${slowest} @@ -95,6 +102,7 @@ class BenchmarkReporter extends JasmineSpecReporter implements jasmine.CustomRep `)); } else { console.log(terminal.colors.yellow(tags.indentBy(6)` + count: ${count} fastest: ${fastest} slowest: ${slowest} mean: ${mean} @@ -120,12 +128,6 @@ global.benchmarkReporter = new BenchmarkReporter(); runner.env.addReporter(global.benchmarkReporter); -// Manually set exit code (needed with custom reporters) -runner.onComplete((success: boolean) => { - process.exitCode = success ? 0 : 1; -}); - - // Run the tests. const allTests = glob.sync('packages/**/*_benchmark.ts') @@ -134,5 +136,8 @@ const allTests = export default function(_args: {}) { - runner.execute(allTests); + return new Promise(resolve => { + runner.onComplete((passed: boolean) => resolve(passed ? 0 : 1)); + runner.execute(allTests); + }); } diff --git a/yarn.lock b/yarn.lock index a3ac15a74c64..35a1e3264287 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6731,6 +6731,10 @@ scss-tokenizer@^0.2.3: js-base64 "^2.1.8" source-map "^0.4.2" +seedrandom@^2.4.4: + version "2.4.4" + resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-2.4.4.tgz#b25ea98632c73e45f58b77cfaa931678df01f9ba" + select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"