url: speed up WHATWG URL parsing · nodejs/node@2a64032 · GitHub
Skip to content

Commit 2a64032

Browse files
anonrigaduh95
authored andcommitted
url: speed up WHATWG URL parsing
Parse one-byte ASCII inputs in place instead of copying them into a UTF-8 buffer, and reuse the original V8 string when the serialized href is unchanged. Delay URLContext allocation until parse finishes and skip ToString when the input is already a string. Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com> Assisted-by: Cursor PR-URL: #65361 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Daniel Lemire <daniel@lemire.me> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
1 parent b71d5de commit 2a64032

3 files changed

Lines changed: 215 additions & 69 deletions

File tree

lib/internal/url.js

Lines changed: 59 additions & 55 deletions

src/node_url.cc

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "node_metadata.h"
99
#include "node_process-inl.h"
1010
#include "path.h"
11+
#include "simdutf.h"
1112
#include "util-inl.h"
1213
#include "v8-fast-api-calls.h"
1314
#include "v8-local-handle.h"
@@ -33,6 +34,38 @@ using v8::SnapshotCreator;
3334
using v8::String;
3435
using v8::Value;
3536

37+
namespace {
38+
39+
// Parse a V8 string as a URL. One-byte ASCII inputs are parsed in place
40+
// without allocating a UTF-8 copy. `reuse_input` is set when the serialized
41+
// href is identical to that ASCII input so the caller can return the original
42+
// V8 string. Non-ASCII inputs are never reused: UTF-8 conversion may replace
43+
// unpaired surrogates, so the original string may not match href.
44+
ada::result<ada::url_aggregator> ParseUrlFromV8String(
45+
Isolate* isolate,
46+
Local<String> input,
47+
const ada::url_aggregator* base_url,
48+
bool* reuse_input) {
49+
{
50+
String::ValueView view(isolate, input);
51+
if (view.is_one_byte()) {
52+
const char* data = reinterpret_cast<const char*>(view.data8());
53+
const size_t length = static_cast<size_t>(view.length());
54+
if (simdutf::validate_ascii(data, length)) [[likely]] {
55+
const std::string_view input_view(data, length);
56+
auto out = ada::parse<ada::url_aggregator>(input_view, base_url);
57+
*reuse_input = out.has_value() && out->get_href() == input_view;
58+
return out;
59+
}
60+
}
61+
}
62+
*reuse_input = false;
63+
Utf8Value utf8(isolate, input);
64+
return ada::parse<ada::url_aggregator>(utf8.ToStringView(), base_url);
65+
}
66+
67+
} // namespace
68+
3669
void BindingData::MemoryInfo(MemoryTracker* tracker) const {
3770
tracker->TrackField("url_components_buffer", url_components_buffer_);
3871
}
@@ -392,32 +425,51 @@ void BindingData::Parse(const FunctionCallbackInfo<Value>& args) {
392425
Realm* realm = Realm::GetCurrent(args);
393426
BindingData* binding_data = realm->GetBindingData<BindingData>();
394427
Isolate* isolate = realm->isolate();
395-
std::optional<std::string> base_{};
428+
Local<String> input_string = args[0].As<String>();
396429

397-
Utf8Value input(isolate, args[0]);
398430
ada::result<ada::url_aggregator> base;
399431
ada::url_aggregator* base_pointer = nullptr;
400432
if (args[1]->IsString()) {
401-
base_ = Utf8Value(isolate, args[1]).ToString();
402-
base = ada::parse<ada::url_aggregator>(*base_);
403-
if (!base && raise_exception) {
404-
return ThrowInvalidURL(realm->env(), input.ToStringView(), base_);
405-
} else if (!base) {
433+
bool unused_reuse = false;
434+
base = ParseUrlFromV8String(
435+
isolate, args[1].As<String>(), nullptr, &unused_reuse);
436+
if (!base) {
437+
if (raise_exception) {
438+
Utf8Value input(isolate, input_string);
439+
Utf8Value base_utf8(isolate, args[1]);
440+
return ThrowInvalidURL(
441+
realm->env(), input.ToStringView(), base_utf8.ToString());
442+
}
406443
return;
407444
}
408445
base_pointer = &base.value();
409446
}
410-
auto out =
411-
ada::parse<ada::url_aggregator>(input.ToStringView(), base_pointer);
412447

413-
if (!out && raise_exception) {
414-
return ThrowInvalidURL(realm->env(), input.ToStringView(), base_);
415-
} else if (!out) {
448+
bool reuse_input = false;
449+
auto out =
450+
ParseUrlFromV8String(isolate, input_string, base_pointer, &reuse_input);
451+
if (!out) {
452+
if (raise_exception) {
453+
Utf8Value input(isolate, input_string);
454+
std::optional<std::string> base_error;
455+
if (args[1]->IsString()) {
456+
base_error = Utf8Value(isolate, args[1]).ToString();
457+
}
458+
return ThrowInvalidURL(
459+
realm->env(), input.ToStringView(), std::move(base_error));
460+
}
416461
return;
417462
}
418463

419464
binding_data->UpdateComponents(out->get_components(), out->type);
420465

466+
// Already-serialized ASCII URLs are the common case. Reuse the input
467+
// string instead of allocating an identical V8 string from href.
468+
if (reuse_input) {
469+
args.GetReturnValue().Set(args[0]);
470+
return;
471+
}
472+
421473
Local<Value> ret;
422474
if (ToV8Value(realm->context(), out->get_href(), isolate).ToLocal(&ret))
423475
[[likely]] {
@@ -439,13 +491,15 @@ void BindingData::Update(const FunctionCallbackInfo<Value>& args) {
439491
return;
440492
}
441493
enum url_update_action action = static_cast<enum url_update_action>(val);
442-
Utf8Value input(isolate, args[0].As<String>());
443494
Utf8Value new_value(isolate, args[2].As<String>());
444495

445496
std::string_view new_value_view = new_value.ToStringView();
446497
// A serialized URL is not always reparsable: the IDNA encoder can emit a
447498
// host label that the decoder rejects. Fail the update instead of crashing.
448-
auto out = ada::parse<ada::url_aggregator>(input.ToStringView());
499+
// Existing hrefs are typically already-serialized ASCII, so parse in place.
500+
bool unused_reuse = false;
501+
auto out = ParseUrlFromV8String(
502+
isolate, args[0].As<String>(), nullptr, &unused_reuse);
449503
if (!out) {
450504
return args.GetReturnValue().Set(false);
451505
}
Lines changed: 88 additions & 0 deletions

0 commit comments

Comments
 (0)