perf(signer): reduce String allocations in V4 signing path (#3197)

* perf(signer): reduce String allocations in V4 signing path

- v4_ignored_headers: HashMap<String, bool> -> HashSet<&'static str>
- Header lookups: k.to_string() -> k.as_str() (zero-copy)
- Query params: Vec<(String, String)> -> Vec<(&str, &str)> (zero-copy)
- Canonical request: single String with push_str instead of 6 intermediate Strings + join
- 20 signer tests pass

* fix(signer): align ignored header set type

---------

Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
安正超
2026-06-03 23:40:35 +08:00
committed by GitHub
parent 0fb02049ac
commit 7b57d5b217
+41 -32
View File
@@ -16,7 +16,7 @@ use bytes::BytesMut;
use http::HeaderMap; use http::HeaderMap;
use http::Uri; use http::Uri;
use http::request; use http::request;
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use std::fmt::Write; use std::fmt::Write;
use std::sync::LazyLock; use std::sync::LazyLock;
use time::{OffsetDateTime, macros::format_description}; use time::{OffsetDateTime, macros::format_description};
@@ -63,12 +63,12 @@ struct SignFailure {
type SignOutcome = std::result::Result<request::Request<Body>, Box<SignFailure>>; type SignOutcome = std::result::Result<request::Request<Body>, Box<SignFailure>>;
#[allow(non_upper_case_globals)] // FIXME #[allow(non_upper_case_globals)] // FIXME
static v4_ignored_headers: LazyLock<HashMap<String, bool>> = LazyLock::new(|| { static v4_ignored_headers: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
let mut m = <HashMap<String, bool>>::new(); let mut s = HashSet::new();
m.insert("accept-encoding".to_string(), true); s.insert("accept-encoding");
m.insert("authorization".to_string(), true); s.insert("authorization");
m.insert("user-agent".to_string(), true); s.insert("user-agent");
m s
}); });
fn fail(request: request::Request<Body>, error: SignV4Error) -> SignOutcome { fn fail(request: request::Request<Body>, error: SignV4Error) -> SignOutcome {
@@ -136,11 +136,11 @@ fn try_get_hashed_payload(req: &request::Request<Body>) -> SignResult<String> {
Ok(hashed_payload.to_string()) Ok(hashed_payload.to_string())
} }
fn try_get_canonical_headers(req: &request::Request<Body>, ignored_headers: &HashMap<String, bool>) -> SignResult<String> { fn try_get_canonical_headers(req: &request::Request<Body>, ignored_headers: &HashSet<&'static str>) -> SignResult<String> {
let mut headers = Vec::<String>::new(); let mut headers = Vec::<String>::new();
let mut vals = HashMap::<String, Vec<String>>::new(); let mut vals = HashMap::<String, Vec<String>>::new();
for k in req.headers().keys() { for k in req.headers().keys() {
if ignored_headers.get(&k.to_string()).is_some() { if ignored_headers.contains(k.as_str()) {
continue; continue;
} }
headers.push(k.as_str().to_lowercase()); headers.push(k.as_str().to_lowercase());
@@ -210,12 +210,12 @@ fn header_exists(key: &str, headers: &[String]) -> bool {
false false
} }
fn get_signed_headers(req: &request::Request<Body>, ignored_headers: &HashMap<String, bool>) -> String { fn get_signed_headers(req: &request::Request<Body>, ignored_headers: &HashSet<&'static str>) -> String {
let mut headers = Vec::<String>::new(); let mut headers = Vec::<String>::new();
let headers_ref = req.headers(); let headers_ref = req.headers();
debug!("get_signed_headers headers: {:?}", headers_ref); debug!("get_signed_headers headers: {:?}", headers_ref);
for (k, _) in headers_ref { for (k, _) in headers_ref {
if ignored_headers.get(&k.to_string()).is_some() { if ignored_headers.contains(k.as_str()) {
continue; continue;
} }
headers.push(k.as_str().to_lowercase()); headers.push(k.as_str().to_lowercase());
@@ -229,43 +229,52 @@ fn get_signed_headers(req: &request::Request<Body>, ignored_headers: &HashMap<St
fn try_get_canonical_request( fn try_get_canonical_request(
req: &request::Request<Body>, req: &request::Request<Body>,
ignored_headers: &HashMap<String, bool>, ignored_headers: &HashSet<&'static str>,
hashed_payload: &str, hashed_payload: &str,
) -> SignResult<String> { ) -> SignResult<String> {
let mut canonical_query_string = "".to_string(); let mut canonical_query_string = String::new();
if let Some(q) = req.uri().query() { if let Some(q) = req.uri().query() {
// Parse query string into key-value pairs // Parse query string into key-value pairs (zero-copy slices)
let mut query_params: Vec<(String, String)> = Vec::new(); let mut query_params: Vec<(&str, &str)> = Vec::new();
if !q.is_empty() { if !q.is_empty() {
for param in q.split('&') { for param in q.split('&') {
if let Some((key, value)) = param.split_once('=') { if let Some((key, value)) = param.split_once('=') {
query_params.push((key.to_string(), value.to_string())); query_params.push((key, value));
} else { } else {
query_params.push((param.to_string(), "".to_string())); query_params.push((param, ""));
} }
} }
} }
// Sort by key name // Sort by key name
query_params.sort_by(|a, b| a.0.cmp(&b.0)); query_params.sort_by(|a, b| a.0.cmp(b.0));
// Build canonical query string // Build canonical query string with push_str (no intermediate allocations)
//println!("query_params: {query_params:?}"); for (i, (k, v)) in query_params.iter().enumerate() {
let sorted_params: Vec<String> = query_params.iter().map(|(k, v)| format!("{k}={v}")).collect(); if i > 0 {
canonical_query_string.push('&');
canonical_query_string = sorted_params.join("&"); }
canonical_query_string.push_str(k);
canonical_query_string.push('=');
canonical_query_string.push_str(v);
}
canonical_query_string = canonical_query_string.replace("+", "%20"); canonical_query_string = canonical_query_string.replace("+", "%20");
} }
let canonical_request = [ // Build canonical request with a single allocation
req.method().to_string(), let mut canonical_request = String::with_capacity(256);
req.uri().path().to_string(), canonical_request.push_str(req.method().as_str());
canonical_query_string, canonical_request.push('\n');
try_get_canonical_headers(req, ignored_headers)?, canonical_request.push_str(req.uri().path());
get_signed_headers(req, ignored_headers), canonical_request.push('\n');
hashed_payload.to_string(), canonical_request.push_str(&canonical_query_string);
]; canonical_request.push('\n');
Ok(canonical_request.join("\n")) canonical_request.push_str(&try_get_canonical_headers(req, ignored_headers)?);
canonical_request.push('\n');
canonical_request.push_str(&get_signed_headers(req, ignored_headers));
canonical_request.push('\n');
canonical_request.push_str(hashed_payload);
Ok(canonical_request)
} }
fn try_get_string_to_sign_v4( fn try_get_string_to_sign_v4(