Skip to content

Commit 64fbfff

Browse files
feat(UrlService): (UrlRouter) improve perf of registering Url Rules and sorting Url Rules
1) The `UrlRouter.rule` function was re-sorting all the rules each time a new one was registered. Now, the rules are sorted only just before they are used (by either `.match()` or `.rules()`). 2) The UrlMatcher.compare function was slow because it re-computed static information each time it ran. Now, the static "segments" data is stored in `UrlMatcher._cache` Closes #27 Closes angular-ui/ui-router#3274
1 parent fdb3ab9 commit 64fbfff

File tree

2 files changed

+59
-34
lines changed

2 files changed

+59
-34
lines changed

src/url/urlMatcher.ts

+43-28
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
/**
22
* @coreapi
33
* @module url
4-
*/ /** for typedoc */
4+
*/
5+
/** for typedoc */
56
import {
6-
map, defaults, inherit, identity, unnest, tail, find, Obj, pairs, allTrueR, unnestR, arrayTuples
7+
map, defaults, inherit, identity, unnest, tail, find, Obj, pairs, allTrueR, unnestR, arrayTuples
78
} from "../common/common";
8-
import { prop, propEq, pattern, eq, is, val } from "../common/hof";
9+
import { prop, propEq } from "../common/hof";
910
import { isArray, isString, isDefined } from "../common/predicates";
1011
import { Param, DefType } from "../params/param";
1112
import { ParamTypes } from "../params/paramTypes";
@@ -35,11 +36,16 @@ function quoteRegExp(string: any, param?: any) {
3536
const memoizeTo = (obj: Obj, prop: string, fn: Function) =>
3637
obj[prop] = obj[prop] || fn();
3738

39+
/** @hidden */
40+
const splitOnSlash = splitOnDelim('/');
41+
3842
/** @hidden */
3943
interface UrlMatcherCache {
40-
path: UrlMatcher[];
41-
parent: UrlMatcher;
42-
pattern: RegExp;
44+
segments?: any[];
45+
weights?: number[];
46+
path?: UrlMatcher[];
47+
parent?: UrlMatcher;
48+
pattern?: RegExp;
4349
}
4450

4551
/**
@@ -98,7 +104,7 @@ export class UrlMatcher {
98104
static nameValidator: RegExp = /^\w+([-.]+\w+)*(?:\[\])?$/;
99105

100106
/** @hidden */
101-
private _cache: UrlMatcherCache = { path: [this], parent: null, pattern: null };
107+
private _cache: UrlMatcherCache = { path: [this] };
102108
/** @hidden */
103109
private _children: UrlMatcher[] = [];
104110
/** @hidden */
@@ -474,8 +480,6 @@ export class UrlMatcher {
474480
* The comparison function sorts static segments before dynamic ones.
475481
*/
476482
static compare(a: UrlMatcher, b: UrlMatcher): number {
477-
const splitOnSlash = splitOnDelim('/');
478-
479483
/**
480484
* Turn a UrlMatcher and all its parent matchers into an array
481485
* of slash literals '/', string literals, and Param objects
@@ -484,27 +488,38 @@ export class UrlMatcher {
484488
* var matcher = $umf.compile("/foo").append($umf.compile("/:param")).append($umf.compile("/")).append($umf.compile("tail"));
485489
* var result = segments(matcher); // [ '/', 'foo', '/', Param, '/', 'tail' ]
486490
*
491+
* Caches the result as `matcher._cache.segments`
487492
*/
488493
const segments = (matcher: UrlMatcher) =>
489-
matcher._cache.path.map(UrlMatcher.pathSegmentsAndParams)
490-
.reduce(unnestR, [])
491-
.reduce(joinNeighborsR, [])
492-
.map(x => isString(x) ? splitOnSlash(x) : x)
493-
.reduce(unnestR, []);
494-
495-
let aSegments = segments(a), bSegments = segments(b);
496-
// console.table( { aSegments, bSegments });
497-
498-
// Sort slashes first, then static strings, the Params
499-
const weight = pattern([
500-
[eq("/"), val(1)],
501-
[isString, val(2)],
502-
[is(Param), val(3)]
503-
]);
504-
let pairs = arrayTuples(aSegments.map(weight), bSegments.map(weight));
505-
// console.table(pairs);
506-
507-
return pairs.reduce((cmp, weightPair) => cmp !== 0 ? cmp : weightPair[0] - weightPair[1], 0);
494+
matcher._cache.segments = matcher._cache.segments ||
495+
matcher._cache.path.map(UrlMatcher.pathSegmentsAndParams)
496+
.reduce(unnestR, [])
497+
.reduce(joinNeighborsR, [])
498+
.map(x => isString(x) ? splitOnSlash(x) : x)
499+
.reduce(unnestR, []);
500+
501+
/**
502+
* Gets the sort weight for each segment of a UrlMatcher
503+
*
504+
* Caches the result as `matcher._cache.weights`
505+
*/
506+
const weights = (matcher: UrlMatcher) =>
507+
matcher._cache.weights = matcher._cache.weights ||
508+
segments(matcher).map(segment => {
509+
// Sort slashes first, then static strings, the Params
510+
if (segment === '/') return 1;
511+
if (isString(segment)) return 2;
512+
if (segment instanceof Param) return 3;
513+
});
514+
515+
let cmp, i, pairs = arrayTuples(weights(a), weights(b));
516+
517+
for (i = 0; i < pairs.length; i++) {
518+
cmp = pairs[i][0] - pairs[i][1];
519+
if (cmp !== 0) return cmp;
520+
}
521+
522+
return 0;
508523
}
509524
}
510525

src/url/urlRouter.ts

+16-6
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ function appendBasePath(url: string, isHtml5: boolean, absolute: boolean, baseHr
2626
/** @hidden */
2727
const getMatcher = prop("urlMatcher");
2828

29-
30-
3129
/**
3230
* Default rule priority sorting function.
3331
*
@@ -71,6 +69,7 @@ export class UrlRouter implements UrlRulesApi, UrlSyncApi, Disposable {
7169
/** @hidden */ private _otherwiseFn: UrlRule;
7270
/** @hidden */ interceptDeferred = false;
7371
/** @hidden */ private _id = 0;
72+
/** @hidden */ private _sorted = false;
7473

7574
/** @hidden */
7675
constructor(router: UIRouter) {
@@ -89,6 +88,11 @@ export class UrlRouter implements UrlRulesApi, UrlSyncApi, Disposable {
8988
/** @inheritdoc */
9089
sort(compareFn?: (a: UrlRule, b: UrlRule) => number) {
9190
this._rules.sort(this._sortFn = compareFn || this._sortFn);
91+
this._sorted = true;
92+
}
93+
94+
private ensureSorted() {
95+
this._sorted || this.sort();
9296
}
9397

9498
/**
@@ -97,6 +101,8 @@ export class UrlRouter implements UrlRulesApi, UrlSyncApi, Disposable {
97101
* @returns {MatchResult}
98102
*/
99103
match(url: UrlParts): MatchResult {
104+
this.ensureSorted();
105+
100106
url = extend({path: '', search: {}, hash: '' }, url);
101107
let rules = this.rules();
102108
if (this._otherwiseFn) rules.push(this._otherwiseFn);
@@ -247,19 +253,23 @@ export class UrlRouter implements UrlRulesApi, UrlSyncApi, Disposable {
247253
if (!UrlRuleFactory.isUrlRule(rule)) throw new Error("invalid rule");
248254
rule.$id = this._id++;
249255
rule.priority = rule.priority || 0;
256+
250257
this._rules.push(rule);
251-
this.sort();
258+
this._sorted = false;
259+
252260
return () => this.removeRule(rule);
253261
}
254262

255263
/** @inheritdoc */
256264
removeRule(rule): void {
257265
removeFrom(this._rules, rule);
258-
this.sort();
259266
}
260267

261268
/** @inheritdoc */
262-
rules(): UrlRule[] { return this._rules.slice(); }
269+
rules(): UrlRule[] {
270+
this.ensureSorted();
271+
return this._rules.slice();
272+
}
263273

264274
/** @inheritdoc */
265275
otherwise(handler: string|UrlRuleHandlerFn|TargetState|TargetStateDef) {
@@ -269,7 +279,7 @@ export class UrlRouter implements UrlRulesApi, UrlSyncApi, Disposable {
269279

270280
let handlerFn: UrlRuleHandlerFn = isFunction(handler) ? handler as UrlRuleHandlerFn : val(handler);
271281
this._otherwiseFn = this.urlRuleFactory.create(val(true), handlerFn);
272-
this.sort();
282+
this._sorted = false;
273283
};
274284

275285
/** @inheritdoc */

0 commit comments

Comments
 (0)