-
-
Notifications
You must be signed in to change notification settings - Fork 36.5k
url: speed up WHATWG URL parsing #65361
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |||||||||||
| #include "node_metadata.h" | ||||||||||||
| #include "node_process-inl.h" | ||||||||||||
| #include "path.h" | ||||||||||||
| #include "simdutf.h" | ||||||||||||
| #include "util-inl.h" | ||||||||||||
| #include "v8-fast-api-calls.h" | ||||||||||||
| #include "v8-local-handle.h" | ||||||||||||
|
|
@@ -33,6 +34,38 @@ using v8::SnapshotCreator; | |||||||||||
| using v8::String; | ||||||||||||
| using v8::Value; | ||||||||||||
|
|
||||||||||||
| namespace { | ||||||||||||
|
|
||||||||||||
| // Parse a V8 string as a URL. One-byte ASCII inputs are parsed in place | ||||||||||||
| // without allocating a UTF-8 copy. `reuse_input` is set when the serialized | ||||||||||||
| // href is identical to that ASCII input so the caller can return the original | ||||||||||||
| // V8 string. Non-ASCII inputs are never reused: UTF-8 conversion may replace | ||||||||||||
| // unpaired surrogates, so the original string may not match href. | ||||||||||||
| ada::result<ada::url_aggregator> ParseUrlFromV8String( | ||||||||||||
| Isolate* isolate, | ||||||||||||
| Local<String> input, | ||||||||||||
| const ada::url_aggregator* base_url, | ||||||||||||
| bool* reuse_input) { | ||||||||||||
| { | ||||||||||||
| String::ValueView view(isolate, input); | ||||||||||||
| if (view.is_one_byte()) { | ||||||||||||
| const char* data = reinterpret_cast<const char*>(view.data8()); | ||||||||||||
| const size_t length = static_cast<size_t>(view.length()); | ||||||||||||
| if (simdutf::validate_ascii(data, length)) [[likely]] { | ||||||||||||
| const std::string_view input_view(data, length); | ||||||||||||
| auto out = ada::parse<ada::url_aggregator>(input_view, base_url); | ||||||||||||
| *reuse_input = out.has_value() && out->get_href() == input_view; | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
| return out; | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| *reuse_input = false; | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
| Utf8Value utf8(isolate, input); | ||||||||||||
| return ada::parse<ada::url_aggregator>(utf8.ToStringView(), base_url); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| } // namespace | ||||||||||||
|
|
||||||||||||
| void BindingData::MemoryInfo(MemoryTracker* tracker) const { | ||||||||||||
| tracker->TrackField("url_components_buffer", url_components_buffer_); | ||||||||||||
| } | ||||||||||||
|
|
@@ -392,32 +425,51 @@ void BindingData::Parse(const FunctionCallbackInfo<Value>& args) { | |||||||||||
| Realm* realm = Realm::GetCurrent(args); | ||||||||||||
| BindingData* binding_data = realm->GetBindingData<BindingData>(); | ||||||||||||
| Isolate* isolate = realm->isolate(); | ||||||||||||
| std::optional<std::string> base_{}; | ||||||||||||
| Local<String> input_string = args[0].As<String>(); | ||||||||||||
|
|
||||||||||||
| Utf8Value input(isolate, args[0]); | ||||||||||||
| ada::result<ada::url_aggregator> base; | ||||||||||||
| ada::url_aggregator* base_pointer = nullptr; | ||||||||||||
| if (args[1]->IsString()) { | ||||||||||||
| base_ = Utf8Value(isolate, args[1]).ToString(); | ||||||||||||
| base = ada::parse<ada::url_aggregator>(*base_); | ||||||||||||
| if (!base && raise_exception) { | ||||||||||||
| return ThrowInvalidURL(realm->env(), input.ToStringView(), base_); | ||||||||||||
| } else if (!base) { | ||||||||||||
| bool unused_reuse = false; | ||||||||||||
| base = ParseUrlFromV8String( | ||||||||||||
| isolate, args[1].As<String>(), nullptr, &unused_reuse); | ||||||||||||
|
Comment on lines
+433
to
+435
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
| if (!base) { | ||||||||||||
| if (raise_exception) { | ||||||||||||
| Utf8Value input(isolate, input_string); | ||||||||||||
| Utf8Value base_utf8(isolate, args[1]); | ||||||||||||
| return ThrowInvalidURL( | ||||||||||||
| realm->env(), input.ToStringView(), base_utf8.ToString()); | ||||||||||||
| } | ||||||||||||
| return; | ||||||||||||
| } | ||||||||||||
| base_pointer = &base.value(); | ||||||||||||
| } | ||||||||||||
| auto out = | ||||||||||||
| ada::parse<ada::url_aggregator>(input.ToStringView(), base_pointer); | ||||||||||||
|
|
||||||||||||
| if (!out && raise_exception) { | ||||||||||||
| return ThrowInvalidURL(realm->env(), input.ToStringView(), base_); | ||||||||||||
| } else if (!out) { | ||||||||||||
| bool reuse_input = false; | ||||||||||||
| auto out = | ||||||||||||
| ParseUrlFromV8String(isolate, input_string, base_pointer, &reuse_input); | ||||||||||||
| if (!out) { | ||||||||||||
| if (raise_exception) { | ||||||||||||
| Utf8Value input(isolate, input_string); | ||||||||||||
| std::optional<std::string> base_error; | ||||||||||||
| if (args[1]->IsString()) { | ||||||||||||
| base_error = Utf8Value(isolate, args[1]).ToString(); | ||||||||||||
| } | ||||||||||||
| return ThrowInvalidURL( | ||||||||||||
| realm->env(), input.ToStringView(), std::move(base_error)); | ||||||||||||
| } | ||||||||||||
| return; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| binding_data->UpdateComponents(out->get_components(), out->type); | ||||||||||||
|
|
||||||||||||
| // Already-serialized ASCII URLs are the common case. Reuse the input | ||||||||||||
| // string instead of allocating an identical V8 string from href. | ||||||||||||
| if (reuse_input) { | ||||||||||||
| args.GetReturnValue().Set(args[0]); | ||||||||||||
| return; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| Local<Value> ret; | ||||||||||||
| if (ToV8Value(realm->context(), out->get_href(), isolate).ToLocal(&ret)) | ||||||||||||
| [[likely]] { | ||||||||||||
|
|
@@ -439,13 +491,15 @@ void BindingData::Update(const FunctionCallbackInfo<Value>& args) { | |||||||||||
| return; | ||||||||||||
| } | ||||||||||||
| enum url_update_action action = static_cast<enum url_update_action>(val); | ||||||||||||
| Utf8Value input(isolate, args[0].As<String>()); | ||||||||||||
| Utf8Value new_value(isolate, args[2].As<String>()); | ||||||||||||
|
|
||||||||||||
| std::string_view new_value_view = new_value.ToStringView(); | ||||||||||||
| // A serialized URL is not always reparsable: the IDNA encoder can emit a | ||||||||||||
| // host label that the decoder rejects. Fail the update instead of crashing. | ||||||||||||
| auto out = ada::parse<ada::url_aggregator>(input.ToStringView()); | ||||||||||||
| // Existing hrefs are typically already-serialized ASCII, so parse in place. | ||||||||||||
| bool unused_reuse = false; | ||||||||||||
| auto out = ParseUrlFromV8String( | ||||||||||||
| isolate, args[0].As<String>(), nullptr, &unused_reuse); | ||||||||||||
|
Comment on lines
+500
to
+502
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
| if (!out) { | ||||||||||||
| return args.GetReturnValue().Set(false); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| 'use strict'; | ||
|
|
||
| // Covers the URL constructor parse paths that avoid a UTF-8 copy and/or | ||
| // reuse the input string when it is already a serialized ASCII href. | ||
|
|
||
| const { hasIntl } = require('../common'); | ||
| const assert = require('assert'); | ||
|
|
||
| const alreadySerialized = [ | ||
| 'https://nodejs.org/en/blog/', | ||
| 'http://nodejs.org:89/docs/latest/api/foo/bar/qua/13949281/0f28b/' + | ||
| '/5d49/b3020/url.html#test?payload1=true&payload2=false&test=1' + | ||
| '&benchmark=3&foo=38.38.011.293&bar=1234834910480&test=19299&3992&' + | ||
| 'key=f5c65e1e98fe07e648249ad41e1cfdb0', | ||
| 'https://user:pass@example.com/path?search=1', | ||
| 'file:///foo/bar/test/node.js', | ||
| 'ws://localhost:9229/f46db715-70df-43ad-a359-7f9949f39868', | ||
| ]; | ||
|
|
||
| for (const href of alreadySerialized) { | ||
| const url = new URL(href); | ||
| assert.strictEqual(url.href, href); | ||
| assert.strictEqual(URL.parse(href).href, href); | ||
| assert.strictEqual(URL.canParse(href), true); | ||
| } | ||
|
|
||
| // Special-scheme URLs with an empty path gain a trailing slash. | ||
| { | ||
| const url = new URL('https://example.com'); | ||
| assert.strictEqual(url.href, 'https://example.com/'); | ||
| assert.strictEqual(url.pathname, '/'); | ||
| } | ||
|
|
||
| // Dot-segment normalization must still rewrite the path. | ||
| { | ||
| const url = new URL('https://example.org/./a/../b/./c'); | ||
| assert.strictEqual(url.href, 'https://example.org/b/c'); | ||
| assert.strictEqual(url.pathname, '/b/c'); | ||
| } | ||
|
|
||
| // Relative resolution against a base URL. | ||
| { | ||
| const url = new URL('/path?x=1#h', 'https://example.com:8443/base'); | ||
| assert.strictEqual(url.href, 'https://example.com:8443/path?x=1#h'); | ||
| assert.strictEqual(url.host, 'example.com:8443'); | ||
| } | ||
|
|
||
| // Non-string input is still stringified. | ||
| { | ||
| const url = new URL({ toString: () => 'https://example.com/from-object' }); | ||
| assert.strictEqual(url.href, 'https://example.com/from-object'); | ||
| } | ||
|
|
||
| // Invalid input still throws from the constructor and is null from parse(). | ||
| { | ||
| assert.throws(() => new URL('not a url'), { | ||
| code: 'ERR_INVALID_URL', | ||
| name: 'TypeError', | ||
| }); | ||
| assert.strictEqual(URL.parse('not a url'), null); | ||
| assert.strictEqual(URL.canParse('not a url'), false); | ||
| } | ||
|
|
||
| // Unpaired surrogates must not be returned as-is from href. | ||
| { | ||
| const input = 'https://example.com/\uD800'; | ||
| const url = new URL(input); | ||
| assert.notStrictEqual(url.href, input); | ||
| assert.ok(url.href.startsWith('https://example.com/')); | ||
| } | ||
|
|
||
| if (hasIntl) { | ||
| const url = new URL('http://你好你好.在线'); | ||
| assert.ok(url.hostname.startsWith('xn--')); | ||
| assert.ok(url.href.startsWith('http://xn--')); | ||
| } | ||
|
|
||
| // Setters re-parse the existing href; keep component updates correct. | ||
| { | ||
| const url = new URL('https://example.com/old'); | ||
| url.pathname = '/new'; | ||
| url.search = 'q=1'; | ||
| url.hash = 'frag'; | ||
| assert.strictEqual(url.href, 'https://example.com/new?q=1#frag'); | ||
| assert.strictEqual(url.pathname, '/new'); | ||
| assert.strictEqual(url.search, '?q=1'); | ||
| assert.strictEqual(url.hash, '#frag'); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.