url: performance improvement in URL implementation

Yields about a 25% average performance improvement

PR-URL: https://github.com/nodejs/node/pull/10469
Reviewed-By: Michaël Zasso <targos@protonmail.com>
This commit is contained in:
James M Snell 2016-12-26 20:56:21 -08:00
parent 509ff1b9e4
commit 2213f3640a

393
lib/internal/url.js Normal file → Executable file
View File

@ -67,50 +67,151 @@ class TupleOrigin {
}
toString(unicode = false) {
var result = this.scheme;
var result = this[kScheme];
result += '://';
result += unicode ? URL.domainToUnicode(this.host) : this.host;
if (this.port !== undefined && this.port !== null)
result += `:${this.port}`;
result += unicode ? URL.domainToUnicode(this[kHost]) : this[kHost];
if (this[kPort] !== undefined && this[kPort] !== null)
result += `:${this[kPort]}`;
return result;
}
inspect() {
return `TupleOrigin {
scheme: ${this.scheme},
host: ${this.host},
port: ${this.port},
domain: ${this.domain}
scheme: ${this[kScheme]},
host: ${this[kHost]},
port: ${this[kPort]},
domain: ${this[kDomain]}
}`;
}
}
function onParseComplete(flags, protocol, username, password,
host, port, path, query, fragment) {
if (flags & binding.URL_FLAGS_FAILED)
throw new TypeError('Invalid URL');
var ctx = this[context];
ctx.flags = flags;
ctx.scheme = protocol;
ctx.username = username;
ctx.password = password;
ctx.port = port;
ctx.path = path;
ctx.query = query;
ctx.fragment = fragment;
ctx.host = host;
if (this[searchParams]) { // invoked from href setter
initSearchParams(this[searchParams], query);
} else {
this[searchParams] = new URLSearchParams(query);
}
this[searchParams][context] = this;
}
// Reused by URL constructor and URL#href setter.
function parse(url, input, base) {
input = String(input);
const base_context = base ? base[context] : undefined;
url[context] = new StorageObject();
binding.parse(input.trim(), -1, base_context, undefined,
(flags, protocol, username, password,
host, port, path, query, fragment) => {
if (flags & binding.URL_FLAGS_FAILED)
throw new TypeError('Invalid URL');
url[context].flags = flags;
url[context].scheme = protocol;
url[context].username = username;
url[context].password = password;
url[context].port = port;
url[context].path = path;
url[context].query = query;
url[context].fragment = fragment;
url[context].host = host;
if (url[searchParams]) { // invoked from href setter
initSearchParams(url[searchParams], query);
} else {
url[searchParams] = new URLSearchParams(query);
}
url[searchParams][context] = url;
});
binding.parse(input.trim(), -1,
base_context, undefined,
onParseComplete.bind(url));
}
function onParseProtocolComplete(flags, protocol, username, password,
host, port, path, query, fragment) {
if (flags & binding.URL_FLAGS_FAILED)
return;
const newIsSpecial = (flags & binding.URL_FLAGS_SPECIAL) !== 0;
const s = this[special];
const ctx = this[context];
if ((s && !newIsSpecial) || (!s && newIsSpecial)) {
return;
}
if (newIsSpecial) {
ctx.flags |= binding.URL_FLAGS_SPECIAL;
} else {
ctx.flags &= ~binding.URL_FLAGS_SPECIAL;
}
if (protocol) {
ctx.scheme = protocol;
ctx.flags |= binding.URL_FLAGS_HAS_SCHEME;
} else {
ctx.flags &= ~binding.URL_FLAGS_HAS_SCHEME;
}
}
function onParseHostComplete(flags, protocol, username, password,
host, port, path, query, fragment) {
if (flags & binding.URL_FLAGS_FAILED)
return;
const ctx = this[context];
if (host) {
ctx.host = host;
ctx.flags |= binding.URL_FLAGS_HAS_HOST;
} else {
ctx.flags &= ~binding.URL_FLAGS_HAS_HOST;
}
if (port !== undefined)
ctx.port = port;
}
function onParseHostnameComplete(flags, protocol, username, password,
host, port, path, query, fragment) {
if (flags & binding.URL_FLAGS_FAILED)
return;
const ctx = this[context];
if (host) {
ctx.host = host;
ctx.flags |= binding.URL_FLAGS_HAS_HOST;
} else {
ctx.flags &= ~binding.URL_FLAGS_HAS_HOST;
}
}
function onParsePortComplete(flags, protocol, username, password,
host, port, path, query, fragment) {
if (flags & binding.URL_FLAGS_FAILED)
return;
this[context].port = port;
}
function onParsePathComplete(flags, protocol, username, password,
host, port, path, query, fragment) {
if (flags & binding.URL_FLAGS_FAILED)
return;
const ctx = this[context];
if (path) {
ctx.path = path;
ctx.flags |= binding.URL_FLAGS_HAS_PATH;
} else {
ctx.flags &= ~binding.URL_FLAGS_HAS_PATH;
}
}
function onParseSearchComplete(flags, protocol, username, password,
host, port, path, query, fragment) {
if (flags & binding.URL_FLAGS_FAILED)
return;
const ctx = this[context];
if (query) {
ctx.query = query;
ctx.flags |= binding.URL_FLAGS_HAS_QUERY;
} else {
ctx.flags &= ~binding.URL_FLAGS_HAS_QUERY;
}
}
function onParseHashComplete(flags, protocol, username, password,
host, port, path, query, fragment) {
if (flags & binding.URL_FLAGS_FAILED)
return;
const ctx = this[context];
if (fragment) {
ctx.fragment = fragment;
ctx.flags |= binding.URL_FLAGS_HAS_FRAGMENT;
} else {
ctx.flags &= ~binding.URL_FLAGS_HAS_FRAGMENT;
}
}
class URL {
@ -121,33 +222,34 @@ class URL {
}
get [special]() {
return (this[context].flags & binding.URL_FLAGS_SPECIAL) != 0;
return (this[context].flags & binding.URL_FLAGS_SPECIAL) !== 0;
}
get [cannotBeBase]() {
return (this[context].flags & binding.URL_FLAGS_CANNOT_BE_BASE) != 0;
return (this[context].flags & binding.URL_FLAGS_CANNOT_BE_BASE) !== 0;
}
inspect(depth, opts) {
const ctx = this[context];
var ret = 'URL {\n';
ret += ` href: ${this.href}\n`;
if (this[context].scheme !== undefined)
if (ctx.scheme !== undefined)
ret += ` protocol: ${this.protocol}\n`;
if (this[context].username !== undefined)
if (ctx.username !== undefined)
ret += ` username: ${this.username}\n`;
if (this[context].password !== undefined) {
const pwd = opts.showHidden ? this[context].password : '--------';
if (ctx.password !== undefined) {
const pwd = opts.showHidden ? ctx.password : '--------';
ret += ` password: ${pwd}\n`;
}
if (this[context].host !== undefined)
if (ctx.host !== undefined)
ret += ` hostname: ${this.hostname}\n`;
if (this[context].port !== undefined)
if (ctx.port !== undefined)
ret += ` port: ${this.port}\n`;
if (this[context].path !== undefined)
if (ctx.path !== undefined)
ret += ` pathname: ${this.pathname}\n`;
if (this[context].query !== undefined)
if (ctx.query !== undefined)
ret += ` search: ${this.search}\n`;
if (this[context].fragment !== undefined)
if (ctx.fragment !== undefined)
ret += ` hash: ${this.hash}\n`;
if (opts.showHidden) {
ret += ` cannot-be-base: ${this[cannotBeBase]}\n`;
@ -171,18 +273,19 @@ Object.defineProperties(URL.prototype, {
options.fragment !== undefined ?
!!options.fragment : true;
const unicode = !!options.unicode;
const ctx = this[context];
var ret;
if (this.protocol)
ret = this.protocol;
if (this[context].host !== undefined) {
if (ctx.host !== undefined) {
ret += '//';
const has_username = typeof this[context].username === 'string';
const has_password = typeof this[context].password === 'string';
const has_username = typeof ctx.username === 'string';
const has_password = typeof ctx.password === 'string';
if (has_username || has_password) {
if (has_username)
ret += this[context].username;
ret += ctx.username;
if (has_password)
ret += `:${this[context].password}`;
ret += `:${ctx.password}`;
ret += '@';
}
if (unicode) {
@ -192,15 +295,15 @@ Object.defineProperties(URL.prototype, {
} else {
ret += this.host;
}
} else if (this[context].scheme === 'file:') {
} else if (ctx.scheme === 'file:') {
ret += '//';
}
if (this.pathname)
ret += this.pathname;
if (typeof this[context].query === 'string')
ret += `?${this[context].query}`;
if (fragment & typeof this[context].fragment === 'string')
ret += `#${this[context].fragment}`;
if (typeof ctx.query === 'string')
ret += `?${ctx.query}`;
if (fragment & typeof ctx.fragment === 'string')
ret += `#${ctx.fragment}`;
return ret;
}
},
@ -231,33 +334,8 @@ Object.defineProperties(URL.prototype, {
scheme = String(scheme);
if (scheme.length === 0)
return;
binding.parse(scheme,
binding.kSchemeStart,
null,
this[context],
(flags, protocol, username, password,
host, port, path, query, fragment) => {
if (flags & binding.URL_FLAGS_FAILED)
return;
const newIsSpecial = (flags & binding.URL_FLAGS_SPECIAL) != 0;
if ((this[special] && !newIsSpecial) ||
(!this[special] && newIsSpecial) ||
(newIsSpecial && !this[special] &&
this[context].host === undefined)) {
return;
}
if (newIsSpecial) {
this[context].flags |= binding.URL_FLAGS_SPECIAL;
} else {
this[context].flags &= ~binding.URL_FLAGS_SPECIAL;
}
if (protocol) {
this[context].scheme = protocol;
this[context].flags |= binding.URL_FLAGS_HAS_SCHEME;
} else {
this[context].flags &= ~binding.URL_FLAGS_HAS_SCHEME;
}
});
binding.parse(scheme, binding.kSchemeStart, null, this[context],
onParseProtocolComplete.bind(this));
}
},
username: {
@ -270,13 +348,14 @@ Object.defineProperties(URL.prototype, {
username = String(username);
if (!this.hostname)
return;
const ctx = this[context];
if (!username) {
this[context].username = null;
this[context].flags &= ~binding.URL_FLAGS_HAS_USERNAME;
ctx.username = null;
ctx.flags &= ~binding.URL_FLAGS_HAS_USERNAME;
return;
}
this[context].username = binding.encodeAuth(username);
this[context].flags |= binding.URL_FLAGS_HAS_USERNAME;
ctx.username = binding.encodeAuth(username);
ctx.flags |= binding.URL_FLAGS_HAS_USERNAME;
}
},
password: {
@ -289,25 +368,28 @@ Object.defineProperties(URL.prototype, {
password = String(password);
if (!this.hostname)
return;
const ctx = this[context];
if (!password) {
this[context].password = null;
this[context].flags &= ~binding.URL_FLAGS_HAS_PASSWORD;
ctx.password = null;
ctx.flags &= ~binding.URL_FLAGS_HAS_PASSWORD;
return;
}
this[context].password = binding.encodeAuth(password);
this[context].flags |= binding.URL_FLAGS_HAS_PASSWORD;
ctx.password = binding.encodeAuth(password);
ctx.flags |= binding.URL_FLAGS_HAS_PASSWORD;
}
},
host: {
enumerable: true,
configurable: true,
get() {
var ret = this[context].host || '';
if (this[context].port !== undefined)
ret += `:${this[context].port}`;
const ctx = this[context];
var ret = ctx.host || '';
if (ctx.port !== undefined)
ret += `:${ctx.port}`;
return ret;
},
set(host) {
const ctx = this[context];
host = String(host);
if (this[cannotBeBase] ||
(this[special] && host.length === 0)) {
@ -316,24 +398,12 @@ Object.defineProperties(URL.prototype, {
return;
}
if (!host) {
this[context].host = null;
this[context].flags &= ~binding.URL_FLAGS_HAS_HOST;
ctx.host = null;
ctx.flags &= ~binding.URL_FLAGS_HAS_HOST;
return;
}
binding.parse(host, binding.kHost, null, this[context],
(flags, protocol, username, password,
host, port, path, query, fragment) => {
if (flags & binding.URL_FLAGS_FAILED)
return;
if (host) {
this[context].host = host;
this[context].flags |= binding.URL_FLAGS_HAS_HOST;
} else {
this[context].flags &= ~binding.URL_FLAGS_HAS_HOST;
}
if (port !== undefined)
this[context].port = port;
});
binding.parse(host, binding.kHost, null, ctx,
onParseHostComplete.bind(this));
}
},
hostname: {
@ -343,6 +413,7 @@ Object.defineProperties(URL.prototype, {
return this[context].host || '';
},
set(host) {
const ctx = this[context];
host = String(host);
if (this[cannotBeBase] ||
(this[special] && host.length === 0)) {
@ -351,25 +422,12 @@ Object.defineProperties(URL.prototype, {
return;
}
if (!host) {
this[context].host = null;
this[context].flags &= ~binding.URL_FLAGS_HAS_HOST;
ctx.host = null;
ctx.flags &= ~binding.URL_FLAGS_HAS_HOST;
return;
}
binding.parse(host,
binding.kHostname,
null,
this[context],
(flags, protocol, username, password,
host, port, path, query, fragment) => {
if (flags & binding.URL_FLAGS_FAILED)
return;
if (host) {
this[context].host = host;
this[context].flags |= binding.URL_FLAGS_HAS_HOST;
} else {
this[context].flags &= ~binding.URL_FLAGS_HAS_HOST;
}
});
binding.parse(host, binding.kHostname, null, ctx,
onParseHostnameComplete.bind(this));
}
},
port: {
@ -380,7 +438,8 @@ Object.defineProperties(URL.prototype, {
return port === undefined ? '' : String(port);
},
set(port) {
if (!this[context].host || this[cannotBeBase] ||
const ctx = this[context];
if (!ctx.host || this[cannotBeBase] ||
this.protocol === 'file:')
return;
port = String(port);
@ -389,76 +448,46 @@ Object.defineProperties(URL.prototype, {
// TODO(jasnell): This might be changing in the spec
return;
}
binding.parse(port, binding.kPort, null, this[context],
(flags, protocol, username, password,
host, port, path, query, fragment) => {
if (flags & binding.URL_FLAGS_FAILED)
return;
this[context].port = port;
});
binding.parse(port, binding.kPort, null, ctx,
onParsePortComplete.bind(this));
}
},
pathname: {
enumerable: true,
configurable: true,
get() {
const ctx = this[context];
if (this[cannotBeBase])
return this[context].path[0];
return this[context].path !== undefined ?
`/${this[context].path.join('/')}` : '';
return ctx.path[0];
return ctx.path !== undefined ? `/${ctx.path.join('/')}` : '';
},
set(path) {
if (this[cannotBeBase])
return;
path = String(path);
binding.parse(path,
binding.kPathStart,
null,
this[context],
(flags, protocol, username, password,
host, port, path, query, fragment) => {
if (flags & binding.URL_FLAGS_FAILED)
return;
if (path) {
this[context].path = path;
this[context].flags |= binding.URL_FLAGS_HAS_PATH;
} else {
this[context].flags &= ~binding.URL_FLAGS_HAS_PATH;
}
});
binding.parse(String(path), binding.kPathStart, null, this[context],
onParsePathComplete.bind(this));
}
},
search: {
enumerable: true,
configurable: true,
get() {
return !this[context].query ? '' : `?${this[context].query}`;
const ctx = this[context];
return !ctx.query ? '' : `?${ctx.query}`;
},
set(search) {
const ctx = this[context];
search = String(search);
if (search[0] === '?') search = search.slice(1);
if (!search) {
this[context].query = null;
this[context].flags &= ~binding.URL_FLAGS_HAS_QUERY;
ctx.query = null;
ctx.flags &= ~binding.URL_FLAGS_HAS_QUERY;
this[searchParams][searchParams] = {};
return;
}
this[context].query = '';
binding.parse(search,
binding.kQuery,
null,
this[context],
(flags, protocol, username, password,
host, port, path, query, fragment) => {
if (flags & binding.URL_FLAGS_FAILED)
return;
if (query) {
this[context].query = query;
this[context].flags |= binding.URL_FLAGS_HAS_QUERY;
} else {
this[context].flags &= ~binding.URL_FLAGS_HAS_QUERY;
}
});
ctx.query = '';
binding.parse(search, binding.kQuery, null, ctx,
onParseSearchComplete.bind(this));
this[searchParams][searchParams] = querystring.parse(search);
}
},
@ -473,39 +502,29 @@ Object.defineProperties(URL.prototype, {
enumerable: true,
configurable: true,
get() {
return !this[context].fragment ? '' : `#${this[context].fragment}`;
const ctx = this[context];
return !ctx.fragment ? '' : `#${ctx.fragment}`;
},
set(hash) {
const ctx = this[context];
hash = String(hash);
if (this.protocol === 'javascript:')
return;
if (!hash) {
this[context].fragment = null;
this[context].flags &= ~binding.URL_FLAGS_HAS_FRAGMENT;
ctx.fragment = null;
ctx.flags &= ~binding.URL_FLAGS_HAS_FRAGMENT;
return;
}
if (hash[0] === '#') hash = hash.slice(1);
this[context].fragment = '';
binding.parse(hash,
binding.kFragment,
null,
this[context],
(flags, protocol, username, password,
host, port, path, query, fragment) => {
if (flags & binding.URL_FLAGS_FAILED)
return;
if (fragment) {
this[context].fragment = fragment;
this[context].flags |= binding.URL_FLAGS_HAS_FRAGMENT;
} else {
this[context].flags &= ~binding.URL_FLAGS_HAS_FRAGMENT;
}
});
ctx.fragment = '';
binding.parse(hash, binding.kFragment, null, ctx,
onParseHashComplete.bind(this));
}
}
});
var hexTable = new Array(256);
const hexTable = new Array(256);
for (var i = 0; i < 256; ++i)
hexTable[i] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase();
function encodeAuth(str) {