Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Normalize cache #12462

Merged
merged 2 commits into from
Oct 3, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions packages/_/benchmark/src/benchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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);

Expand All @@ -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,
Expand All @@ -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;
Expand Down
7 changes: 5 additions & 2 deletions packages/angular_devkit/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
35 changes: 34 additions & 1 deletion packages/angular_devkit/core/src/virtual-fs/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,18 +185,51 @@ 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<string, Path>();


/**
* Reset the cache. This is only useful for testing.
* @private
*/
export function resetNormalizeCache() {
normalizedCache = new Map<string, Path>();
}


/**
* 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 `/`.
* - Multiple `/` are replaced by a single one.
* - 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) {
Expand Down
74 changes: 71 additions & 3 deletions packages/angular_devkit/core/src/virtual-fs/path_benchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]));
});
});
});
21 changes: 13 additions & 8 deletions scripts/benchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ declare const global: {


interface BenchmarkResult {
count: number;
slowest: number[];
fastest: number[];
mean: number;
Expand Down Expand Up @@ -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('');
Expand All @@ -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}
Expand All @@ -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}
Expand All @@ -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')
Expand All @@ -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);
});
}
4 changes: 4 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down