Skip to content

Commit 786b341

Browse files
authored
perf: allocate template caches lazily (#307)
1 parent 95cf774 commit 786b341

8 files changed

Lines changed: 299 additions & 21 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ let parser = NetflowParser::builder()
214214
```
215215

216216
**Cache Behavior:**
217+
- Empty template caches reserve no entry storage; each cache grows as templates arrive
217218
- When the cache is full, the least recently used template is evicted
218219
- Templates are keyed by template ID (per source)
219220
- Each parser instance maintains its own template cache

src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ use std::sync::Arc;
2727

2828
/// Count non-expired templates in an LRU cache, respecting TTL if configured.
2929
fn count_valid_templates<T>(
30-
cache: &lru::LruCache<u16, variable_versions::ttl::TemplateWithTtl<T>>,
30+
cache: &variable_versions::lazy_lru::LazyLruCache<
31+
u16,
32+
variable_versions::ttl::TemplateWithTtl<T>,
33+
>,
3134
ttl_config: &Option<variable_versions::ttl::TtlConfig>,
3235
) -> usize {
3336
match ttl_config {

src/variable_versions/ipfix/parser.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use crate::template_store::{
1919
use crate::variable_versions::config::DEFAULT_MAX_RECORDS_PER_FLOWSET;
2020
use crate::variable_versions::enterprise_registry::EnterpriseFieldRegistry;
2121
use crate::variable_versions::field_value::FieldValue;
22+
use crate::variable_versions::lazy_lru::LazyLruCache;
2223
use crate::variable_versions::metrics::CacheMetricsInner;
2324
use crate::variable_versions::template_events::TemplateProtocol;
2425
use crate::variable_versions::ttl::{TemplateWithTtl, TtlConfig};
@@ -33,7 +34,6 @@ use crate::variable_versions::{
3334
use crate::{NetflowError, NetflowPacket, ParsedNetflow};
3435

3536
use crate::variable_versions::fast_parse::{parse_u8, parse_u16_be};
36-
use lru::LruCache;
3737
use nom::IResult;
3838
use nom::combinator::complete;
3939
use nom::multi::many0;
@@ -88,10 +88,10 @@ impl IPFixParser {
8888
.transpose()?;
8989

9090
Ok(Self {
91-
templates: LruCache::new(cache_size),
92-
v9_templates: LruCache::new(cache_size),
93-
ipfix_options_templates: LruCache::new(cache_size),
94-
v9_options_templates: LruCache::new(cache_size),
91+
templates: LazyLruCache::new(cache_size),
92+
v9_templates: LazyLruCache::new(cache_size),
93+
ipfix_options_templates: LazyLruCache::new(cache_size),
94+
v9_options_templates: LazyLruCache::new(cache_size),
9595
ttl_config: config.ttl_config,
9696
max_template_cache_size: config.max_template_cache_size,
9797
max_field_count: config.max_field_count,
@@ -159,7 +159,7 @@ impl IPFixParser {
159159
/// checker reaching for splits.
160160
#[allow(clippy::too_many_arguments)]
161161
fn install_restored<T>(
162-
cache: &mut LruCache<crate::variable_versions::TemplateId, TemplateWithTtl<Arc<T>>>,
162+
cache: &mut LazyLruCache<crate::variable_versions::TemplateId, TemplateWithTtl<Arc<T>>>,
163163
template_id: u16,
164164
arc: &Arc<T>,
165165
ttl_enabled: bool,
@@ -835,7 +835,7 @@ impl HasTemplateId for V9OptionsTemplate {
835835
/// metrics. Returns the IDs of any entries the LRU evicted to make room so the
836836
/// caller can mirror the eviction into the secondary template store.
837837
fn insert_templates<T: HasTemplateId>(
838-
cache: &mut LruCache<u16, TemplateWithTtl<Arc<T>>>,
838+
cache: &mut LazyLruCache<u16, TemplateWithTtl<Arc<T>>>,
839839
templates: &[T],
840840
ttl_enabled: bool,
841841
metrics: &mut CacheMetricsInner,

src/variable_versions/ipfix/types.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::template_store::TemplateStore;
99
use crate::variable_versions::PendingFlowCache;
1010
use crate::variable_versions::enterprise_registry::EnterpriseFieldRegistry;
1111
use crate::variable_versions::field_value::FieldValue;
12+
use crate::variable_versions::lazy_lru::LazyLruCache;
1213
use crate::variable_versions::metrics::CacheMetricsInner;
1314
use crate::variable_versions::template_events::TemplateProtocol;
1415
use crate::variable_versions::ttl::{TemplateWithTtl, TtlConfig};
@@ -24,7 +25,6 @@ use crate::variable_versions::v9::{
2425
Template as V9Template,
2526
};
2627

27-
use lru::LruCache;
2828
use std::sync::Arc;
2929

3030
use crate::variable_versions::TemplateId;
@@ -37,12 +37,12 @@ pub type IPFixFlowRecord = Vec<IPFixFieldPair>;
3737
/// Supports both native IPFIX templates and V9-style templates embedded in IPFIX messages.
3838
#[derive(Debug)]
3939
pub struct IPFixParser {
40-
pub(crate) templates: LruCache<TemplateId, TemplateWithTtl<Arc<Template>>>,
41-
pub(crate) v9_templates: LruCache<TemplateId, TemplateWithTtl<Arc<V9Template>>>,
40+
pub(crate) templates: LazyLruCache<TemplateId, TemplateWithTtl<Arc<Template>>>,
41+
pub(crate) v9_templates: LazyLruCache<TemplateId, TemplateWithTtl<Arc<V9Template>>>,
4242
pub(crate) ipfix_options_templates:
43-
LruCache<TemplateId, TemplateWithTtl<Arc<OptionsTemplate>>>,
43+
LazyLruCache<TemplateId, TemplateWithTtl<Arc<OptionsTemplate>>>,
4444
pub(crate) v9_options_templates:
45-
LruCache<TemplateId, TemplateWithTtl<Arc<V9OptionsTemplate>>>,
45+
LazyLruCache<TemplateId, TemplateWithTtl<Arc<V9OptionsTemplate>>>,
4646
pub(crate) ttl_config: Option<TtlConfig>,
4747
pub(crate) max_template_cache_size: usize,
4848
pub(crate) max_field_count: usize,

src/variable_versions/lazy_lru.rs

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
use lru::LruCache;
2+
use std::borrow::Borrow;
3+
use std::fmt;
4+
use std::hash::Hash;
5+
use std::num::NonZeroUsize;
6+
7+
/// An LRU cache that allocates its backing storage on first insertion.
8+
///
9+
/// `lru::LruCache::new(capacity)` reserves the full configured capacity.
10+
/// Template parsers own several independent caches, most of which remain unused
11+
/// for a given exporter. This wrapper retains the same capacity and eviction
12+
/// semantics without reserving memory for empty caches.
13+
pub(crate) struct LazyLruCache<K: Hash + Eq, V> {
14+
capacity: NonZeroUsize,
15+
cache: Option<LruCache<K, V>>,
16+
}
17+
18+
impl<K: Hash + Eq, V> fmt::Debug for LazyLruCache<K, V> {
19+
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
20+
// Preserve the parser's existing debug representation.
21+
formatter
22+
.debug_struct("LruCache")
23+
.field("len", &self.len())
24+
.field("cap", &self.capacity)
25+
.finish()
26+
}
27+
}
28+
29+
impl<K: Hash + Eq, V> LazyLruCache<K, V> {
30+
pub(crate) fn new(capacity: NonZeroUsize) -> Self {
31+
Self {
32+
capacity,
33+
cache: None,
34+
}
35+
}
36+
37+
fn cache_mut(&mut self) -> &mut LruCache<K, V> {
38+
let capacity = self.capacity;
39+
self.cache.get_or_insert_with(|| {
40+
// `unbounded()` starts with an empty hash map. Resizing it before
41+
// the first insertion sets the eviction limit without reserving
42+
// storage for every possible entry.
43+
let mut cache = LruCache::unbounded();
44+
cache.resize(capacity);
45+
cache
46+
})
47+
}
48+
49+
fn release_if_empty(&mut self) {
50+
if self.cache.as_ref().is_some_and(LruCache::is_empty) {
51+
self.cache = None;
52+
}
53+
}
54+
55+
pub(crate) fn push(&mut self, key: K, value: V) -> Option<(K, V)> {
56+
self.cache_mut().push(key, value)
57+
}
58+
59+
#[cfg(test)]
60+
pub(crate) fn put(&mut self, key: K, value: V) -> Option<V> {
61+
self.cache_mut().put(key, value)
62+
}
63+
64+
pub(crate) fn peek<Q>(&self, key: &Q) -> Option<&V>
65+
where
66+
K: Borrow<Q>,
67+
Q: Hash + Eq + ?Sized,
68+
{
69+
self.cache.as_ref()?.peek(key)
70+
}
71+
72+
pub(crate) fn pop<Q>(&mut self, key: &Q) -> Option<V>
73+
where
74+
K: Borrow<Q>,
75+
Q: Hash + Eq + ?Sized,
76+
{
77+
let value = self.cache.as_mut()?.pop(key);
78+
self.release_if_empty();
79+
value
80+
}
81+
82+
pub(crate) fn promote<Q>(&mut self, key: &Q) -> bool
83+
where
84+
K: Borrow<Q>,
85+
Q: Hash + Eq + ?Sized,
86+
{
87+
self.cache.as_mut().is_some_and(|cache| cache.promote(key))
88+
}
89+
90+
pub(crate) fn resize(&mut self, capacity: NonZeroUsize) {
91+
self.capacity = capacity;
92+
if let Some(cache) = self.cache.as_mut() {
93+
cache.resize(capacity);
94+
}
95+
self.release_if_empty();
96+
}
97+
98+
pub(crate) fn clear(&mut self) {
99+
self.cache = None;
100+
}
101+
102+
pub(crate) fn len(&self) -> usize {
103+
self.cache.as_ref().map_or(0, LruCache::len)
104+
}
105+
106+
#[cfg(test)]
107+
pub(crate) fn cap(&self) -> NonZeroUsize {
108+
self.capacity
109+
}
110+
111+
pub(crate) fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
112+
self.cache.iter().flat_map(|cache| cache.iter())
113+
}
114+
}
115+
116+
#[cfg(test)]
117+
mod tests {
118+
use super::*;
119+
120+
#[test]
121+
fn reads_and_resizes_do_not_allocate_an_empty_cache() {
122+
let mut cache = LazyLruCache::<u16, u16>::new(NonZeroUsize::new(10).unwrap());
123+
124+
assert!(cache.cache.is_none());
125+
assert_eq!(cache.cap().get(), 10);
126+
assert_eq!(cache.len(), 0);
127+
assert_eq!(cache.peek(&1), None);
128+
assert!(!cache.promote(&1));
129+
assert_eq!(cache.pop(&1), None);
130+
assert_eq!(cache.iter().count(), 0);
131+
132+
cache.resize(NonZeroUsize::new(20).unwrap());
133+
assert!(cache.cache.is_none());
134+
assert_eq!(cache.cap().get(), 20);
135+
}
136+
137+
#[test]
138+
fn first_insert_allocates_and_preserves_lru_eviction() {
139+
let mut cache = LazyLruCache::new(NonZeroUsize::new(2).unwrap());
140+
141+
assert_eq!(cache.push(1, "one"), None);
142+
assert!(cache.cache.is_some());
143+
assert_eq!(cache.push(2, "two"), None);
144+
assert!(cache.promote(&1));
145+
assert_eq!(cache.push(3, "three"), Some((2, "two")));
146+
assert_eq!(cache.peek(&1), Some(&"one"));
147+
assert_eq!(cache.peek(&2), None);
148+
assert_eq!(cache.peek(&3), Some(&"three"));
149+
}
150+
151+
#[test]
152+
fn removing_the_last_entry_releases_storage() {
153+
let mut cache = LazyLruCache::new(NonZeroUsize::new(2).unwrap());
154+
cache.put(1, "one");
155+
156+
assert_eq!(cache.pop(&1), Some("one"));
157+
assert!(cache.cache.is_none());
158+
159+
cache.put(2, "two");
160+
cache.clear();
161+
assert!(cache.cache.is_none());
162+
assert_eq!(cache.cap().get(), 2);
163+
}
164+
}

src/variable_versions/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ pub(crate) mod fast_parse;
7373
pub mod field_types;
7474
pub mod field_value;
7575
pub mod ipfix;
76+
pub(crate) mod lazy_lru;
7677
pub mod metrics;
7778
pub(crate) mod pending_flows;
7879
pub mod template_events;
@@ -144,7 +145,7 @@ pub(crate) fn calculate_padding(body_size: usize) -> &'static [u8] {
144145
/// Returns None if the template doesn't exist or has expired.
145146
#[inline]
146147
pub(crate) fn get_valid_template<T: Clone>(
147-
cache: &mut lru::LruCache<TemplateId, ttl::TemplateWithTtl<std::sync::Arc<T>>>,
148+
cache: &mut lazy_lru::LazyLruCache<TemplateId, ttl::TemplateWithTtl<std::sync::Arc<T>>>,
148149
id: &TemplateId,
149150
ttl_config: &Option<TtlConfig>,
150151
metrics: &mut CacheMetricsInner,
@@ -170,7 +171,7 @@ pub(crate) fn get_valid_template<T: Clone>(
170171
/// Used in replay paths where the parse may fail — promotion should only
171172
/// happen on a successful replay (callers promote manually on success).
172173
pub(crate) fn peek_valid_template<T: Clone>(
173-
cache: &mut lru::LruCache<TemplateId, ttl::TemplateWithTtl<std::sync::Arc<T>>>,
174+
cache: &mut lazy_lru::LazyLruCache<TemplateId, ttl::TemplateWithTtl<std::sync::Arc<T>>>,
174175
id: &TemplateId,
175176
ttl_config: &Option<TtlConfig>,
176177
metrics: &mut CacheMetricsInner,

src/variable_versions/v9/parser.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use crate::template_store::{
1919
use crate::variable_versions::config::DEFAULT_MAX_RECORDS_PER_FLOWSET;
2020
use crate::variable_versions::enterprise_registry::EnterpriseFieldRegistry;
2121
use crate::variable_versions::field_value::FieldValue;
22+
use crate::variable_versions::lazy_lru::LazyLruCache;
2223
use crate::variable_versions::metrics::CacheMetricsInner;
2324
use crate::variable_versions::template_events::TemplateProtocol;
2425
use crate::variable_versions::ttl::{TemplateWithTtl, TtlConfig};
@@ -28,7 +29,6 @@ use crate::variable_versions::{
2829
};
2930
use crate::{NetflowError, NetflowPacket, ParsedNetflow};
3031

31-
use lru::LruCache;
3232
use nom::IResult;
3333
use nom::bytes::complete::take;
3434
use nom::error::{Error as NomError, ErrorKind};
@@ -39,8 +39,9 @@ use std::sync::Arc;
3939
/// Stateful NetFlow V9 parser with LRU template caching and optional pending flow support.
4040
#[derive(Debug)]
4141
pub struct V9Parser {
42-
pub(crate) templates: LruCache<TemplateId, TemplateWithTtl<Arc<Template>>>,
43-
pub(crate) options_templates: LruCache<TemplateId, TemplateWithTtl<Arc<OptionsTemplate>>>,
42+
pub(crate) templates: LazyLruCache<TemplateId, TemplateWithTtl<Arc<Template>>>,
43+
pub(crate) options_templates:
44+
LazyLruCache<TemplateId, TemplateWithTtl<Arc<OptionsTemplate>>>,
4445
pub(crate) ttl_config: Option<TtlConfig>,
4546
pub(crate) max_template_cache_size: usize,
4647
pub(crate) max_field_count: usize,
@@ -108,8 +109,8 @@ impl V9Parser {
108109
.transpose()?;
109110

110111
Ok(Self {
111-
templates: LruCache::new(cache_size),
112-
options_templates: LruCache::new(cache_size),
112+
templates: LazyLruCache::new(cache_size),
113+
options_templates: LazyLruCache::new(cache_size),
113114
ttl_config: config.ttl_config,
114115
max_template_cache_size: config.max_template_cache_size,
115116
max_field_count: config.max_field_count,
@@ -191,7 +192,7 @@ impl V9Parser {
191192
/// borrows at the call site without re-borrowing the whole parser.
192193
#[allow(clippy::too_many_arguments)]
193194
fn install_restored_template<T>(
194-
cache: &mut LruCache<TemplateId, TemplateWithTtl<Arc<T>>>,
195+
cache: &mut LazyLruCache<TemplateId, TemplateWithTtl<Arc<T>>>,
195196
template_id: u16,
196197
arc: &Arc<T>,
197198
ttl_enabled: bool,

0 commit comments

Comments
 (0)