Skip to content

Commit 1b71247

Browse files
olivielpeauclaude
andcommitted
perf(tag_filterlist): reduce allocations and use foldhash in hot paths
- Use foldhash for CompiledFilters and inner tag HashSets - Avoid String allocation on name lookup (pass &str via Borrow<str>) - Replace insert_tag (O(n) dedup) with extend (direct Vec push) since source tags are already unique - Skip context/Arc allocations when filtering produces no changes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2a1b918 commit 1b71247

1 file changed

Lines changed: 46 additions & 20 deletions

File tree

  • lib/saluki-components/src/transforms/tag_filterlist

lib/saluki-components/src/transforms/tag_filterlist/mod.rs

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
//! Remote Config.
88
99
use async_trait::async_trait;
10+
use foldhash::fast::RandomState as FoldHashState;
1011
use hashbrown::{HashMap, HashSet};
1112
use memory_accounting::{MemoryBounds, MemoryBoundsBuilder};
1213
use saluki_config::GenericConfiguration;
@@ -46,19 +47,20 @@ pub struct MetricTagFilterEntry {
4647
}
4748

4849
/// Compiled filter table: metric name → (is_exclude, set of tag key names).
49-
pub type CompiledFilters = HashMap<String, (bool, HashSet<String>)>;
50+
pub type CompiledFilters = HashMap<String, (bool, HashSet<String, FoldHashState>), FoldHashState>;
5051

5152
/// Compile a slice of filter entries into an O(1)-lookup table.
5253
///
5354
/// Merge rules:
5455
/// - Same metric name + same action → union of tag key sets.
5556
/// - Same metric name + conflicting actions → `exclude` wins.
5657
pub fn compile_filters(entries: &[MetricTagFilterEntry]) -> CompiledFilters {
57-
let mut filters: CompiledFilters = HashMap::new();
58+
let mut filters: CompiledFilters = HashMap::with_hasher(FoldHashState::default());
5859

5960
for entry in entries {
6061
let is_exclude = entry.action == FilterAction::Exclude;
61-
let tag_set: HashSet<String> = entry.tags.iter().cloned().collect();
62+
let mut tag_set = HashSet::with_capacity_and_hasher(entry.tags.len(), FoldHashState::default());
63+
tag_set.extend(entry.tags.iter().cloned());
6264

6365
match filters.entry(entry.metric_name.clone()) {
6466
hashbrown::hash_map::Entry::Vacant(e) => {
@@ -178,44 +180,68 @@ impl Transform for TagFilterlist {
178180
}
179181
}
180182

181-
/// Applies a tag filter to a shared tag set, returning a new `TagSet` with the filter applied.
183+
/// Applies a tag filter to a shared tag set, returning `Some(TagSet)` if any tags were
184+
/// filtered out, or `None` if the result would be identical to the source.
182185
///
183186
/// Tags whose key is in `names` are excluded when `is_exclude` is true, or kept when false.
184-
/// Always constructs a fresh `TagSet` without mutating the source, preserving isolation for
187+
/// Constructs a fresh `TagSet` without mutating the source, preserving isolation for
185188
/// metrics that share the same underlying `Arc<TagSet>`.
186-
fn apply_tag_filter(tags: &SharedTagSet, is_exclude: bool, names: &HashSet<String>) -> TagSet {
187-
let mut out = TagSet::with_capacity(tags.len());
189+
#[inline]
190+
fn apply_tag_filter(tags: &SharedTagSet, is_exclude: bool, names: &HashSet<String, FoldHashState>) -> Option<TagSet> {
191+
let capacity = if is_exclude {
192+
tags.len().saturating_sub(names.len())
193+
} else {
194+
names.len().min(tags.len())
195+
};
196+
let mut out = TagSet::with_capacity(capacity);
197+
let mut any_change = false;
188198
for tag in tags {
189-
let in_list = names.contains(tag.name());
190-
// XOR: keep if (exclude ∧ not-in-list) ∨ (include ∧ in-list)
191-
if is_exclude != in_list {
192-
out.insert_tag(tag.clone());
199+
if is_exclude != names.contains(tag.name()) {
200+
out.extend([tag.clone()]);
201+
} else {
202+
any_change = true;
193203
}
194204
}
195-
out
205+
if any_change {
206+
Some(out)
207+
} else {
208+
None
209+
}
196210
}
197211

198212
/// Filter the tags of a distribution metric according to the compiled filter table.
199213
///
200214
/// Both instrumented tags and origin tags are filtered using the same tag key list.
201215
/// If the metric name is not present in `filters`, the metric is left unchanged.
216+
/// If filtering would not change any tags, the metric context is left untouched (zero allocations).
217+
#[inline]
202218
pub fn filter_metric_tags(metric: &mut saluki_core::data_model::event::metric::Metric, filters: &CompiledFilters) {
203-
let name = metric.context().name().as_ref().to_owned();
204-
let Some((is_exclude, tag_names)) = filters.get(&name) else {
219+
let Some((is_exclude, tag_names)) = filters.get(metric.context().name().as_ref()) else {
205220
return;
206221
};
207222

208223
let new_tags = apply_tag_filter(metric.context().tags(), *is_exclude, tag_names);
209224

210225
if metric.context().origin_tags().is_empty() {
211-
// Fast path: no origin_tags to filter; single allocation.
212-
*metric.context_mut() = metric.context().with_tags(new_tags.into_shared());
226+
if let Some(filtered) = new_tags {
227+
*metric.context_mut() = metric.context().with_tags(filtered.into_shared());
228+
}
213229
} else {
214-
// Filter origin_tags with the same list; single Arc allocation for both.
215230
let new_origin = apply_tag_filter(metric.context().origin_tags(), *is_exclude, tag_names);
216-
*metric.context_mut() = metric
217-
.context()
218-
.with_tags_and_origin_tags(new_tags.into_shared(), new_origin.into_shared());
231+
match (new_tags, new_origin) {
232+
(None, None) => {}
233+
(Some(tags), None) => {
234+
*metric.context_mut() = metric.context().with_tags(tags.into_shared());
235+
}
236+
(None, Some(origin)) => {
237+
*metric.context_mut() = metric.context().with_origin_tags(origin.into_shared());
238+
}
239+
(Some(tags), Some(origin)) => {
240+
*metric.context_mut() = metric
241+
.context()
242+
.with_tags_and_origin_tags(tags.into_shared(), origin.into_shared());
243+
}
244+
}
219245
}
220246
}
221247

0 commit comments

Comments
 (0)