This is an automated email from the ASF dual-hosted git repository.
andygrove pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-comet.git
The following commit(s) were added to refs/heads/main by this push:
new b9cbb6882b perf: optimize parse_url in datafusion-comet-spark-expr
(#4893)
b9cbb6882b is described below
commit b9cbb6882b6aa2f5c7e6a77dd63ddf471d44ece9
Author: Andy Grove <[email protected]>
AuthorDate: Sat Jul 11 11:20:21 2026 -0600
perf: optimize parse_url in datafusion-comet-spark-expr (#4893)
---
native/spark-expr/Cargo.toml | 4 ++
native/spark-expr/benches/parse_url.rs | 74 ++++++++++++++++++++++++++++
native/spark-expr/src/url_funcs/parse_url.rs | 32 +++++++++---
3 files changed, 103 insertions(+), 7 deletions(-)
diff --git a/native/spark-expr/Cargo.toml b/native/spark-expr/Cargo.toml
index 4addc026d8..ef00bef1ea 100644
--- a/native/spark-expr/Cargo.toml
+++ b/native/spark-expr/Cargo.toml
@@ -131,3 +131,7 @@ harness = false
[[bench]]
name = "array_size"
harness = false
+
+[[bench]]
+name = "parse_url"
+harness = false
diff --git a/native/spark-expr/benches/parse_url.rs
b/native/spark-expr/benches/parse_url.rs
new file mode 100644
index 0000000000..5be9c36f86
--- /dev/null
+++ b/native/spark-expr/benches/parse_url.rs
@@ -0,0 +1,74 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use arrow::array::StringArray;
+use arrow::datatypes::{DataType, Field};
+use criterion::{criterion_group, criterion_main, Criterion};
+use datafusion::common::config::ConfigOptions;
+use datafusion::logical_expr::{ColumnarValue, ScalarFunctionArgs,
ScalarUDFImpl};
+use datafusion_comet_spark_expr::url_funcs::CometParseUrl;
+use std::sync::Arc;
+
+const N: usize = 10_000;
+
+fn call(udf: &CometParseUrl, args: Vec<ColumnarValue>) -> ColumnarValue {
+ let return_field = Arc::new(Field::new("parse_url", DataType::Utf8, true));
+ let sfa = ScalarFunctionArgs {
+ args,
+ number_rows: N,
+ return_field,
+ config_options: Arc::new(ConfigOptions::default()),
+ arg_fields: vec![],
+ };
+ udf.invoke_with_args(sfa).unwrap()
+}
+
+fn criterion_benchmark(c: &mut Criterion) {
+ let mut group = c.benchmark_group("parse_url");
+
+ let udf = CometParseUrl::default();
+
+ // The common Spark shape: extract a query parameter using a constant key
+ // (`parse_url(col, 'QUERY', 'v')`). The key is broadcast to every row.
+ let urls = StringArray::from(
+ (0..N)
+ .map(|i|
format!("https://example.com/path?a={i}&v=value{i}&b={i}"))
+ .collect::<Vec<_>>(),
+ );
+ let urls = ColumnarValue::Array(Arc::new(urls));
+ let part =
ColumnarValue::Scalar(datafusion::common::ScalarValue::Utf8(Some(
+ "QUERY".to_string(),
+ )));
+ let key =
ColumnarValue::Scalar(datafusion::common::ScalarValue::Utf8(Some("v".to_string())));
+
+ group.bench_function("query_key", |b| {
+ b.iter(|| call(&udf, vec![urls.clone(), part.clone(), key.clone()]));
+ });
+
+ group.finish();
+}
+
+fn config() -> Criterion {
+ Criterion::default()
+}
+
+criterion_group! {
+ name = benches;
+ config = config();
+ targets = criterion_benchmark
+}
+criterion_main!(benches);
diff --git a/native/spark-expr/src/url_funcs/parse_url.rs
b/native/spark-expr/src/url_funcs/parse_url.rs
index 25d4ca94fe..7e072b6895 100644
--- a/native/spark-expr/src/url_funcs/parse_url.rs
+++ b/native/spark-expr/src/url_funcs/parse_url.rs
@@ -21,6 +21,7 @@
//! which diverges from Spark's java.net.URI (RFC 3986) on several edge cases.
//! This module uses RFC 3986 Appendix B regex parsing to match Spark exactly.
+use std::cell::RefCell;
use std::sync::{Arc, LazyLock};
use arrow::array::{
@@ -73,16 +74,33 @@ fn extract_userinfo(authority: &str) -> Option<String> {
authority.rfind('@').map(|pos| authority[..pos].to_string())
}
+thread_local! {
+ // The query regex is derived solely from `key`, which is almost always
+ // constant across a batch (e.g. `parse_url(col, 'QUERY', 'v')`). Naive
code
+ // recompiles it for every row; cache the last (key, compiled regex) pair
so
+ // it is compiled once per distinct key instead. A `None` regex records
that
+ // `key` failed to compile as a pattern.
+ static QUERY_KEY_REGEX: RefCell<Option<(String, Option<Regex>)>> =
+ const { RefCell::new(None) };
+}
+
fn extract_query_value(query: &str, key: &str) -> Option<String> {
// Spark uses Pattern.compile("(&|^)" + key + "=([^&]*)") with no escaping,
// so the key is treated as a regex pattern.
- let pattern = format!("(&|^){}=([^&]*)", key);
- match Regex::new(&pattern) {
- Ok(re) => re
- .captures(query)
- .and_then(|caps| caps.get(2).map(|m| m.as_str().to_string())),
- Err(_) => None,
- }
+ QUERY_KEY_REGEX.with(|cell| {
+ let mut slot = cell.borrow_mut();
+ // Drop the cached entry when the key changes, then (re)build lazily.
+ if slot.as_ref().map(|(k, _)| k.as_str()) != Some(key) {
+ *slot = None;
+ }
+ let (_, re) = slot.get_or_insert_with(|| {
+ let pattern = format!("(&|^){}=([^&]*)", key);
+ (key.to_string(), Regex::new(&pattern).ok())
+ });
+ let re = re.as_ref()?;
+ re.captures(query)
+ .and_then(|caps| caps.get(2).map(|m| m.as_str().to_string()))
+ })
}
fn has_invalid_uri_chars(s: &str) -> bool {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]