Skip to content

Commit c13cbd4

Browse files
committed
refactor(@angular-devkit/core): add cache to normalize
And a benchmark to show the curve, more or less.
1 parent ae9695a commit c13cbd4

File tree

4 files changed

+114
-6
lines changed

4 files changed

+114
-6
lines changed

packages/angular_devkit/core/package.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
"ajv": "6.5.3",
1212
"chokidar": "2.0.4",
1313
"fast-json-stable-stringify": "2.0.0",
14-
"source-map": "0.7.3",
15-
"rxjs": "6.3.3"
14+
"rxjs": "6.3.3",
15+
"source-map": "0.7.3"
16+
},
17+
"devDependencies": {
18+
"seedrandom": "^2.4.4"
1619
}
1720
}

packages/angular_devkit/core/src/virtual-fs/path.ts

+34-1
Original file line numberDiff line numberDiff line change
@@ -185,18 +185,51 @@ export function fragment(path: string): PathFragment {
185185
}
186186

187187

188+
/**
189+
* normalize() cache to reduce computation. For now this grows and we never flush it, but in the
190+
* future we might want to add a few cache flush to prevent this from growing too large.
191+
*/
192+
let normalizedCache = new Map<string, Path>();
193+
194+
195+
/**
196+
* Reset the cache. This is only useful for testing.
197+
* @private
198+
*/
199+
export function resetNormalizeCache() {
200+
normalizedCache = new Map<string, Path>();
201+
}
202+
203+
188204
/**
189205
* Normalize a string into a Path. This is the only mean to get a Path type from a string that
190-
* represents a system path. Normalization includes:
206+
* represents a system path. This method cache the results as real world paths tend to be
207+
* duplicated often.
208+
* Normalization includes:
191209
* - Windows backslashes `\\` are replaced with `/`.
192210
* - Windows drivers are replaced with `/X/`, where X is the drive letter.
193211
* - Absolute paths starts with `/`.
194212
* - Multiple `/` are replaced by a single one.
195213
* - Path segments `.` are removed.
196214
* - Path segments `..` are resolved.
197215
* - If a path is absolute, having a `..` at the start is invalid (and will throw).
216+
* @param path The path to be normalized.
198217
*/
199218
export function normalize(path: string): Path {
219+
let maybePath = normalizedCache.get(path);
220+
if (!maybePath) {
221+
maybePath = noCacheNormalize(path);
222+
normalizedCache.set(path, maybePath);
223+
}
224+
225+
return maybePath;
226+
}
227+
228+
229+
/**
230+
* The no cache version of the normalize() function. Used for benchmarking and testing.
231+
*/
232+
export function noCacheNormalize(path: string): Path {
200233
if (path == '' || path == '.') {
201234
return '' as Path;
202235
} else if (path == NormalizedRoot) {

packages/angular_devkit/core/src/virtual-fs/path_benchmark.ts

+71-3
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,82 @@
77
*/
88
// tslint:disable:no-implicit-dependencies
99
import { benchmark } from '@_/benchmark';
10-
import { join, normalize } from './path';
10+
import { join, noCacheNormalize, normalize, resetNormalizeCache } from './path';
11+
12+
const seedrandom = require('seedrandom');
1113

1214

1315
const p1 = '/b/././a/tt/../../../a/b/./d/../c';
1416
const p2 = '/a/tt/../../../a/b/./d';
1517

1618

19+
const numRandomIter = 10000;
20+
21+
1722
describe('Virtual FS Path', () => {
18-
benchmark('normalize', () => normalize(p1));
19-
benchmark('join', () => join(normalize(p1), normalize(p2)));
23+
benchmark('join', () => join(normalize(p1), p2));
24+
25+
describe('normalize', () => {
26+
let rng: () => number;
27+
let cases: string[];
28+
29+
// Math.random() doesn't allow us to set a seed, so we use a library.
30+
beforeEach(() => {
31+
rng = seedrandom('some fixed value');
32+
33+
function _str(len: number) {
34+
let r = '';
35+
const space = 'abcdefghijklmnopqrstuvwxyz0123456789';
36+
for (let i = 0; i < len; i++) {
37+
r += space[Math.floor(rng() * space.length)];
38+
}
39+
40+
return r;
41+
}
42+
43+
// Build test cases.
44+
cases = new Array(numRandomIter)
45+
.fill(0)
46+
.map(() => {
47+
return new Array(Math.floor(rng() * 20 + 5))
48+
.fill(0)
49+
.map(() => _str(rng() * 20 + 3))
50+
.join('/');
51+
});
52+
53+
resetNormalizeCache();
54+
});
55+
56+
describe('random (0 cache hits)', () => {
57+
benchmark('', i => normalize(cases[i]), i => noCacheNormalize(cases[i]));
58+
});
59+
60+
describe('random (10% cache hits)', () => {
61+
beforeEach(() => {
62+
cases = cases.map(x => (rng() < 0.1) ? cases[0] : x);
63+
});
64+
benchmark('', i => normalize(cases[i]), i => noCacheNormalize(cases[i]));
65+
});
66+
67+
describe('random (30% cache hits)', () => {
68+
beforeEach(() => {
69+
cases = cases.map(x => (rng() < 0.3) ? cases[0] : x);
70+
});
71+
benchmark('', i => normalize(cases[i]), i => noCacheNormalize(cases[i]));
72+
});
73+
74+
describe('random (50% cache hits)', () => {
75+
beforeEach(() => {
76+
cases = cases.map(x => (rng() < 0.5) ? cases[0] : x);
77+
});
78+
benchmark('', i => normalize(cases[i]), i => noCacheNormalize(cases[i]));
79+
});
80+
81+
describe('random (80% cache hits)', () => {
82+
beforeEach(() => {
83+
cases = cases.map(x => (rng() < 0.8) ? cases[0] : x);
84+
});
85+
benchmark('', i => normalize(cases[i]), i => noCacheNormalize(cases[i]));
86+
});
87+
});
2088
});

yarn.lock

+4
Original file line numberDiff line numberDiff line change
@@ -6731,6 +6731,10 @@ scss-tokenizer@^0.2.3:
67316731
js-base64 "^2.1.8"
67326732
source-map "^0.4.2"
67336733

6734+
seedrandom@^2.4.4:
6735+
version "2.4.4"
6736+
resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-2.4.4.tgz#b25ea98632c73e45f58b77cfaa931678df01f9ba"
6737+
67346738
select-hose@^2.0.0:
67356739
version "2.0.0"
67366740
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"

0 commit comments

Comments
 (0)