Skip to content

Commit

Permalink
feat(UrlService): (UrlRouter) improve perf of registering Url Rules…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
christopherthielen committed Jan 31, 2017
1 parent fdb3ab9 commit 64fbfff
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 34 deletions.
71 changes: 43 additions & 28 deletions src/url/urlMatcher.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
/**
* @coreapi
* @module url
*/ /** for typedoc */
*/
/** for typedoc */
import {
map, defaults, inherit, identity, unnest, tail, find, Obj, pairs, allTrueR, unnestR, arrayTuples
map, defaults, inherit, identity, unnest, tail, find, Obj, pairs, allTrueR, unnestR, arrayTuples
} from "../common/common";
import { prop, propEq, pattern, eq, is, val } from "../common/hof";
import { prop, propEq } from "../common/hof";
import { isArray, isString, isDefined } from "../common/predicates";
import { Param, DefType } from "../params/param";
import { ParamTypes } from "../params/paramTypes";
Expand Down Expand Up @@ -35,11 +36,16 @@ function quoteRegExp(string: any, param?: any) {
const memoizeTo = (obj: Obj, prop: string, fn: Function) =>
obj[prop] = obj[prop] || fn();

/** @hidden */
const splitOnSlash = splitOnDelim('/');

/** @hidden */
interface UrlMatcherCache {
path: UrlMatcher[];
parent: UrlMatcher;
pattern: RegExp;
segments?: any[];
weights?: number[];
path?: UrlMatcher[];
parent?: UrlMatcher;
pattern?: RegExp;
}

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

/** @hidden */
private _cache: UrlMatcherCache = { path: [this], parent: null, pattern: null };
private _cache: UrlMatcherCache = { path: [this] };
/** @hidden */
private _children: UrlMatcher[] = [];
/** @hidden */
Expand Down Expand Up @@ -474,8 +480,6 @@ export class UrlMatcher {
* The comparison function sorts static segments before dynamic ones.
*/
static compare(a: UrlMatcher, b: UrlMatcher): number {
const splitOnSlash = splitOnDelim('/');

/**
* Turn a UrlMatcher and all its parent matchers into an array
* of slash literals '/', string literals, and Param objects
Expand All @@ -484,27 +488,38 @@ export class UrlMatcher {
* var matcher = $umf.compile("/foo").append($umf.compile("/:param")).append($umf.compile("/")).append($umf.compile("tail"));
* var result = segments(matcher); // [ '/', 'foo', '/', Param, '/', 'tail' ]
*
* Caches the result as `matcher._cache.segments`
*/
const segments = (matcher: UrlMatcher) =>
matcher._cache.path.map(UrlMatcher.pathSegmentsAndParams)
.reduce(unnestR, [])
.reduce(joinNeighborsR, [])
.map(x => isString(x) ? splitOnSlash(x) : x)
.reduce(unnestR, []);

let aSegments = segments(a), bSegments = segments(b);
// console.table( { aSegments, bSegments });

// Sort slashes first, then static strings, the Params
const weight = pattern([
[eq("/"), val(1)],
[isString, val(2)],
[is(Param), val(3)]
]);
let pairs = arrayTuples(aSegments.map(weight), bSegments.map(weight));
// console.table(pairs);

return pairs.reduce((cmp, weightPair) => cmp !== 0 ? cmp : weightPair[0] - weightPair[1], 0);
matcher._cache.segments = matcher._cache.segments ||
matcher._cache.path.map(UrlMatcher.pathSegmentsAndParams)
.reduce(unnestR, [])
.reduce(joinNeighborsR, [])
.map(x => isString(x) ? splitOnSlash(x) : x)
.reduce(unnestR, []);

/**
* Gets the sort weight for each segment of a UrlMatcher
*
* Caches the result as `matcher._cache.weights`
*/
const weights = (matcher: UrlMatcher) =>
matcher._cache.weights = matcher._cache.weights ||
segments(matcher).map(segment => {
// Sort slashes first, then static strings, the Params
if (segment === '/') return 1;
if (isString(segment)) return 2;
if (segment instanceof Param) return 3;
});

let cmp, i, pairs = arrayTuples(weights(a), weights(b));

for (i = 0; i < pairs.length; i++) {
cmp = pairs[i][0] - pairs[i][1];
if (cmp !== 0) return cmp;
}

return 0;
}
}

Expand Down
22 changes: 16 additions & 6 deletions src/url/urlRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ function appendBasePath(url: string, isHtml5: boolean, absolute: boolean, baseHr
/** @hidden */
const getMatcher = prop("urlMatcher");



/**
* Default rule priority sorting function.
*
Expand Down Expand Up @@ -71,6 +69,7 @@ export class UrlRouter implements UrlRulesApi, UrlSyncApi, Disposable {
/** @hidden */ private _otherwiseFn: UrlRule;
/** @hidden */ interceptDeferred = false;
/** @hidden */ private _id = 0;
/** @hidden */ private _sorted = false;

/** @hidden */
constructor(router: UIRouter) {
Expand All @@ -89,6 +88,11 @@ export class UrlRouter implements UrlRulesApi, UrlSyncApi, Disposable {
/** @inheritdoc */
sort(compareFn?: (a: UrlRule, b: UrlRule) => number) {
this._rules.sort(this._sortFn = compareFn || this._sortFn);
this._sorted = true;
}

private ensureSorted() {
this._sorted || this.sort();
}

/**
Expand All @@ -97,6 +101,8 @@ export class UrlRouter implements UrlRulesApi, UrlSyncApi, Disposable {
* @returns {MatchResult}
*/
match(url: UrlParts): MatchResult {
this.ensureSorted();

url = extend({path: '', search: {}, hash: '' }, url);
let rules = this.rules();
if (this._otherwiseFn) rules.push(this._otherwiseFn);
Expand Down Expand Up @@ -247,19 +253,23 @@ export class UrlRouter implements UrlRulesApi, UrlSyncApi, Disposable {
if (!UrlRuleFactory.isUrlRule(rule)) throw new Error("invalid rule");
rule.$id = this._id++;
rule.priority = rule.priority || 0;

this._rules.push(rule);
this.sort();
this._sorted = false;

return () => this.removeRule(rule);
}

/** @inheritdoc */
removeRule(rule): void {
removeFrom(this._rules, rule);
this.sort();
}

/** @inheritdoc */
rules(): UrlRule[] { return this._rules.slice(); }
rules(): UrlRule[] {
this.ensureSorted();
return this._rules.slice();
}

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

let handlerFn: UrlRuleHandlerFn = isFunction(handler) ? handler as UrlRuleHandlerFn : val(handler);
this._otherwiseFn = this.urlRuleFactory.create(val(true), handlerFn);
this.sort();
this._sorted = false;
};

/** @inheritdoc */
Expand Down

0 comments on commit 64fbfff

Please sign in to comment.