From e210efad9efa486e2362e397fcece5ab3457a8a7 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Mon, 26 Dec 2016 20:56:21 -0800 Subject: [PATCH] url: performance improvement in URL implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Yields about a 25% average performance improvement PR-URL: https://github.com/nodejs/node/pull/10469 Reviewed-By: Michaƫl Zasso --- lib/internal/url.js | 393 +++++++++++++++++++++++--------------------- 1 file changed, 206 insertions(+), 187 deletions(-) mode change 100644 => 100755 lib/internal/url.js diff --git a/lib/internal/url.js b/lib/internal/url.js old mode 100644 new mode 100755 index 9d7448be8f6a20..28d5a073e1c124 --- a/lib/internal/url.js +++ b/lib/internal/url.js @@ -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) {