Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion benchmarks/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,19 @@ if(MSVC AND BUILD_SHARED_LIBS)
"$<TARGET_FILE_DIR:percent_encode>") # <--this is out-file path
endif()

# Percent Decode
add_executable(percent_decode percent_decode.cpp)
target_link_libraries(percent_decode PRIVATE ada counters::counters)
target_include_directories(percent_decode PUBLIC "$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>")
target_include_directories(percent_decode PUBLIC "$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/benchmarks>")
if(MSVC AND BUILD_SHARED_LIBS)
# Copy the ada dll into the directory
add_custom_command(TARGET percent_decode POST_BUILD # Adds a post-build event
COMMAND ${CMAKE_COMMAND} -E copy_if_different # which executes "cmake -E copy_if_different..."
"$<TARGET_FILE:ada>" # <--this is in-file
"$<TARGET_FILE_DIR:percent_decode>") # <--this is out-file path
endif()

add_executable(model_bench model_bench.cpp)
target_link_libraries(model_bench PRIVATE ada counters::counters)
target_compile_definitions(model_bench PRIVATE ADA_URL_FILE="${url-dataset_SOURCE_DIR}/out.txt")
Expand All @@ -78,10 +91,11 @@ target_link_libraries(benchdata PRIVATE benchmark::benchmark)
target_link_libraries(bbc_bench PRIVATE benchmark::benchmark)
target_link_libraries(bench_ipv4 PRIVATE benchmark::benchmark)
target_link_libraries(percent_encode PRIVATE benchmark::benchmark)
target_link_libraries(percent_decode PRIVATE benchmark::benchmark)
target_link_libraries(bench_search_params PRIVATE benchmark::benchmark)
target_link_libraries(urlpattern PRIVATE benchmark::benchmark)

set(BENCHMARKS wpt_bench bench benchdata bbc_bench bench_ipv4 percent_encode bench_search_params urlpattern)
set(BENCHMARKS wpt_bench bench benchdata bbc_bench bench_ipv4 percent_encode percent_decode bench_search_params urlpattern)

add_custom_target(run_all_benchmarks
COMMAND ${CMAKE_COMMAND} -E echo "Running all benchmarks..."
Expand Down
92 changes: 92 additions & 0 deletions benchmarks/percent_decode.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#include <memory>

#include "ada.h"
#include "ada/character_sets.h"
#include "ada/unicode.h"
#include "counters/event_counter.h"
counters::event_collector collector;
size_t N = 1000;

#include <benchmark/benchmark.h>

std::string examples[] = {
"utm_source=twc%20mobile&name=John%20Doe&ref=web-twc-ao-gbl",
"https%3A%2F%2Fexample.com%2Fpath%3Fa%3D1%26b%3D2%26c%3D3%26d%3D4",
"the/quick/brown/fox/jumps/over/the/lazy/dog%2Ehtml",
"caf%C3%A9%20%E4%BD%A0%E5%A5%BD%20%F0%9F%98%80%20done"};

void init_data() {}

double examples_bytes = []() -> double {
size_t bytes{0};
for (std::string& url_string : examples) {
bytes += url_string.size();
}
return double(bytes);
}();

static void Decode(benchmark::State& state) {
for (auto _ : state) {
for (std::string& url_string : examples) {
benchmark::DoNotOptimize(
ada::unicode::percent_decode(url_string, url_string.find('%')));
}
}
if (collector.has_events()) {
counters::event_aggregate aggregate{};
for (size_t i = 0; i < N; i++) {
std::atomic_thread_fence(std::memory_order_acquire);
collector.start();
for (std::string& url_string : examples) {
benchmark::DoNotOptimize(
ada::unicode::percent_decode(url_string, url_string.find('%')));
}
std::atomic_thread_fence(std::memory_order_release);
counters::event_count allocate_count = collector.end();
aggregate << allocate_count;
}
state.counters["instructions/url"] =
aggregate.best.instructions() / std::size(examples);
state.counters["instructions/cycle"] =
aggregate.total.instructions() / aggregate.total.cycles();
state.counters["instructions/byte"] =
aggregate.best.instructions() / examples_bytes;
state.counters["GHz"] =
aggregate.total.cycles() / aggregate.total.elapsed_ns();
}
state.counters["time/byte"] = benchmark::Counter(
examples_bytes, benchmark::Counter::kIsIterationInvariantRate |
benchmark::Counter::kInvert);
state.counters["time/url"] =
benchmark::Counter(double(std::size(examples)),
benchmark::Counter::kIsIterationInvariantRate |
benchmark::Counter::kInvert);
state.counters["speed"] = benchmark::Counter(
examples_bytes, benchmark::Counter::kIsIterationInvariantRate);
state.counters["url/s"] =
benchmark::Counter(double(std::size(examples)),
benchmark::Counter::kIsIterationInvariantRate);
}
BENCHMARK(Decode);

int main(int argc, char** argv) {
#if defined(ADA_RUST_VERSION)
benchmark::AddCustomContext("rust version ", ADA_RUST_VERSION);
#endif
#if (__APPLE__ && __aarch64__) || defined(__linux__)
if (!collector.has_events()) {
benchmark::AddCustomContext("performance counters",
"No privileged access (sudo may help).");
}
#else
if (!collector.has_events()) {
benchmark::AddCustomContext("performance counters", "Unsupported system.");
}
#endif
if (collector.has_events()) {
benchmark::AddCustomContext("performance counters", "Enabled");
}
benchmark::Initialize(&argc, argv);
benchmark::RunSpecifiedBenchmarks();
benchmark::Shutdown();
}
64 changes: 42 additions & 22 deletions src/unicode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -461,31 +461,51 @@ std::string percent_decode(const std::string_view input, size_t first_percent) {
if (first_percent == std::string_view::npos) {
return std::string(input);
}
std::string dest;
dest.reserve(input.length());
dest.append(input.substr(0, first_percent));
const char* pointer = input.data() + first_percent;
const char* end = input.data() + input.size();
// Optimization opportunity: if the following code gets
// called often, it can be optimized quite a bit.
while (pointer < end) {
const char ch = pointer[0];
size_t remaining = end - pointer - 1;
if (ch != '%' || remaining < 2 ||
( // ch == '%' && // It is unnecessary to check that ch == '%'.
(!is_ascii_hex_digit(pointer[1]) ||
!is_ascii_hex_digit(pointer[2])))) {
dest += ch;
pointer++;

// NOLINTNEXTLINE(bugprone-suspicious-stringview-data-usage)
const char* const src = input.data();
const char* const end = src + input.size();

// Decoding never grows the string, so a single pre-sized buffer written via
// bulk memcpy of the plain runs (then shrunk to the final length) avoids the
// byte-at-a-time appends of the naive version.
std::string out(input.size(), '\0');
char* d = out.data();
char* const d0 = d;

std::memcpy(d, src, first_percent);
d += first_percent;

const char* p = src + first_percent;
while (p < end) {
if (*p == '%') {
// Decode runs of valid %XX tightly (common for nested/encoded URLs).
while (p + 2 < end && *p == '%') {
if (!is_ascii_hex_digit(p[1]) || !is_ascii_hex_digit(p[2])) {
break;
}
*d++ = static_cast<char>(convert_hex_to_binary(p[1]) * 16 +
convert_hex_to_binary(p[2]));
p += 3;
}
if (p < end && *p == '%') {
// Not a valid escape (too few chars left or bad hex): copy '%'
// literally and keep scanning after it.
*d++ = *p++;
}
} else {
unsigned a = convert_hex_to_binary(pointer[1]);
unsigned b = convert_hex_to_binary(pointer[2]);
char c = static_cast<char>(a * 16 + b);
dest += c;
pointer += 3;
const char* q = static_cast<const char*>(
std::memchr(p, '%', static_cast<size_t>(end - p)));
const char* run_end = q ? q : end;
const size_t n = static_cast<size_t>(run_end - p);
std::memcpy(d, p, n);
d += n;
p = run_end;
}
}
return dest;

out.resize(static_cast<size_t>(d - d0));
return out;
}

// 0..15 for hex digits, 0xFF otherwise - validate and decode with two loads.
Expand Down
20 changes: 20 additions & 0 deletions tests/basic_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,26 @@ TEST(basic_tests, can_parse_consistency_percent_encoded_host) {
}
}

// ada::unicode::percent_decode
TEST(basic_tests, percent_decode_direct) {
using ada::unicode::percent_decode;
constexpr auto npos = std::string_view::npos;
// No percent sign: first_percent == npos returns the input unchanged.
ASSERT_EQ(percent_decode("no percent here", npos), "no percent here");
ASSERT_EQ(percent_decode("", npos), "");
// Valid escapes are decoded.
ASSERT_EQ(percent_decode("a%2Eb", 1), "a.b");
ASSERT_EQ(percent_decode("%41%42%43", 0), "ABC");
ASSERT_EQ(percent_decode("caf%C3%A9", 3), std::string("caf\xc3\xa9"));
// A plain run followed by an escape exercises the memchr run-copy.
ASSERT_EQ(percent_decode("hello%20world", 5), "hello world");
// Invalid escapes are copied literally.
ASSERT_EQ(percent_decode("%zz", 0), "%zz"); // non-hex digits
ASSERT_EQ(percent_decode("x%2", 1), "x%2"); // truncated escape at end
ASSERT_EQ(percent_decode("100%", 3), "100%"); // trailing '%'
ASSERT_EQ(percent_decode("%%41", 0), "%A"); // '%' then a valid escape
}

// Regression: try_can_parse_absolute_fast returned true for a valid IPv4 host
// without validating the port. For "wS://1.3.3.51.:+" the host "1.3.3.51."
// passes the IPv4 fast path, but the port "+" is not a valid digit, so the
Expand Down
Loading