From 22c217da5415fb020d42b947539fc8de24d58603 Mon Sep 17 00:00:00 2001 From: Pierre Tondereau Date: Wed, 18 Feb 2026 22:42:36 +0100 Subject: [PATCH] refactor: use string buffer instead of `format!` --- Cargo.toml | 4 +++ benches/string_alloc_benchmark.rs | 53 +++++++++++++++++++++++++++++++ src/http/query.rs | 14 +++++--- src/marker/mod.rs | 24 +++++++------- 4 files changed, 78 insertions(+), 17 deletions(-) create mode 100644 benches/string_alloc_benchmark.rs diff --git a/Cargo.toml b/Cargo.toml index 9a89238..681cf68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,6 +80,10 @@ harness = false name = "test_examples_benchmark" harness = false +[[bench]] +name = "string_alloc_benchmark" +harness = false + [dependencies.web-sys] version = "0.3.85" features = [ diff --git a/benches/string_alloc_benchmark.rs b/benches/string_alloc_benchmark.rs new file mode 100644 index 0000000..52ddf47 --- /dev/null +++ b/benches/string_alloc_benchmark.rs @@ -0,0 +1,53 @@ +use std::hint::black_box; + +use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; +use redirectionio::{RouterConfig, api::VariableValue, http::PathAndQueryWithSkipped, marker::StaticOrDynamic}; + +fn static_or_dynamic_replace(c: &mut Criterion) { + let mut group = c.benchmark_group("StaticOrDynamic::replace"); + + for &count in &[2, 5, 10, 20] { + let variables: Vec<(String, VariableValue)> = (0..count) + .map(|i| (format!("var{i}"), VariableValue::Value(format!("value{i}")))) + .collect(); + + let template: String = (0..count).map(|i| format!("/path/@var{i}/segment")).collect(); + + group.bench_with_input(BenchmarkId::from_parameter(count), &(template, variables), |b, (tpl, vars)| { + b.iter(|| StaticOrDynamic::replace(tpl.clone(), black_box(vars), false)); + }); + } + + group.finish(); +} + +fn path_and_query_with_skipped_from_config(c: &mut Criterion) { + let mut group = c.benchmark_group("PathAndQueryWithSkipped::from_config"); + + let urls: &[(&str, &str)] = &[ + ("/simple-path", "simple"), + ("/path?a=1&b=2&c=3", "3_params"), + ("/path?a=1&b=2&c=3&d=4&e=5&f=6&g=7&h=8&i=9&j=10", "10_params"), + ( + "/path?a=1&b=2&c=3&d=4&e=5&f=6&g=7&h=8&i=9&j=10&utm_source=google&utm_medium=cpc&utm_campaign=brand&utm_term=test&utm_content=ad1", + "10_params_5_marketing", + ), + ( + "/path?key=hello+world&special=%3Cscript%3E&long_value=Lorem+ipsum+dolor+sit+amet+consectetur+adipiscing+elit", + "special_chars", + ), + ]; + + let config = RouterConfig::default(); + + for &(url, label) in urls { + group.bench_with_input(BenchmarkId::from_parameter(label), &url, |b, &url| { + b.iter(|| PathAndQueryWithSkipped::from_config(&config, url)); + }); + } + + group.finish(); +} + +criterion_group!(benches, static_or_dynamic_replace, path_and_query_with_skipped_from_config,); +criterion_main!(benches); diff --git a/src/http/query.rs b/src/http/query.rs index bb9f425..34c9424 100644 --- a/src/http/query.rs +++ b/src/http/query.rs @@ -1,3 +1,5 @@ +use std::fmt::Write; + use http::uri::PathAndQuery; use linked_hash_map::LinkedHashMap; use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode}; @@ -68,10 +70,10 @@ impl PathAndQueryWithSkipped { }; let mut new_path_and_query = path_and_query.path().to_string(); - let mut skipped_query_params = "".to_string(); + let mut skipped_query_params = String::new(); if let Some(query) = path_and_query.query() { - let mut query_string = "".to_string(); + let mut query_string = String::new(); if config.ignore_all_query_parameters { skipped_query_params = query.to_string(); @@ -83,15 +85,17 @@ impl PathAndQueryWithSkipped { keys.sort(); } + let mut query_param = String::new(); + for key in &keys { let value = hash_query.get(key).unwrap(); - let mut query_param = "".to_string(); - query_param.push_str(&utf8_percent_encode(key, QUERY_ENCODE_SET).to_string()); + query_param.clear(); + let _ = write!(query_param, "{}", utf8_percent_encode(key, QUERY_ENCODE_SET)); if !value.is_empty() { query_param.push('='); - query_param.push_str(&utf8_percent_encode(value, QUERY_ENCODE_SET).to_string()); + let _ = write!(query_param, "{}", utf8_percent_encode(value, QUERY_ENCODE_SET)); } if config.marketing_query_params.contains(key) { diff --git a/src/marker/mod.rs b/src/marker/mod.rs index 241191d..404b3df 100644 --- a/src/marker/mod.rs +++ b/src/marker/mod.rs @@ -2,6 +2,7 @@ mod transformer; use std::{ collections::HashMap, + fmt::Write, sync::{Arc, RwLock}, }; @@ -44,15 +45,12 @@ impl Marker { impl MarkerString { pub fn new(str: &str, mut markers: Vec, ignore_case: bool) -> Option { - // Create regex string let mut regex = regex::escape(str); let mut capture = regex.clone(); let mut marker_map = HashMap::new(); - // Sort markers by length markers.sort_by(|a, b| b.name.len().cmp(&a.name.len())); - // Foreach marker replace for marker in &markers { let marker_regex = format!("(?:{})", marker.regex); let marker_capture = format!("(?P<{}>{})", marker.name, marker.regex); @@ -146,16 +144,18 @@ impl StaticOrDynamic { } pub fn replace(mut str: String, variables: &[(String, VariableValue)], use_default: bool) -> String { + let mut marker_buf = String::new(); + for (name, value) in variables { - match value { - VariableValue::Value(v) => { - str = str.replace(format!("@{name}").as_str(), v.as_str()); - } - VariableValue::HtmlFilter { default: Some(v), .. } if use_default => { - str = str.replace(format!("@{name}").as_str(), v.as_str()); - } - _ => {} - } + let replacement = match value { + VariableValue::Value(v) => v.as_str(), + VariableValue::HtmlFilter { default: Some(v), .. } if use_default => v.as_str(), + _ => continue, + }; + + marker_buf.clear(); + let _ = write!(marker_buf, "@{name}"); + str = str.replace(marker_buf.as_str(), replacement); } str