use crate::{
KnownHeaderName,
headers::{
entry_name::{EntryName, PseudoHeaderName},
field_section::FieldLineValue,
static_hit::StaticHit,
},
};
use hashbrown::HashSet;
use smallvec::SmallVec;
use std::{
fmt::{self, Debug},
sync::Mutex,
};
const ENTRY_OVERHEAD: u32 = 32;
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
#[allow(dead_code)] pub(crate) enum HeaderCompression {
Hpack,
Qpack,
}
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub(in crate::headers) enum NameKey {
Known(KnownHeaderName),
Pseudo(PseudoHeaderName),
UnknownStatic(&'static str),
}
impl Debug for NameKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Known(arg0) => write!(f, "{arg0}"),
Self::Pseudo(arg0) => write!(f, "{arg0}"),
Self::UnknownStatic(arg0) => write!(f, "{arg0:?}"),
}
}
}
impl NameKey {
fn into_entry_name(self) -> EntryName<'static> {
match self {
Self::Known(k) => EntryName::Known(k),
Self::Pseudo(p) => EntryName::Pseudo(p),
Self::UnknownStatic(s) => EntryName::UnknownStatic(s),
}
}
}
#[derive(Debug, Default)]
pub(crate) struct HeaderObserver {
inner: Mutex<ObserverInner>,
}
#[derive(Default)]
struct ObserverInner {
seen_pairs: HashSet<(NameKey, &'static [u8])>,
seen_names: HashSet<NameKey>,
}
impl Debug for ObserverInner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ObserverInner")
.field(
"seen_pairs",
&fmt::from_fn(|f| {
let mut map = f.debug_map();
for (name, value) in &self.seen_pairs {
map.entry(&name, &format_args!("{}", String::from_utf8_lossy(value)));
}
map.finish()?;
Ok(())
}),
)
.field("seen_names", &self.seen_names)
.finish()
}
}
impl HeaderObserver {
pub(in crate::headers) fn fold_connection(&self, accum: &ConnectionAccumulator) {
let Ok(mut inner) = self.inner.lock() else {
return;
};
let pairs_before = inner.seen_pairs.len();
let names_before = inner.seen_names.len();
for &pair in &accum.seen_pairs {
inner.seen_pairs.insert(pair);
}
for &name in &accum.seen_names {
inner.seen_names.insert(name);
}
let pairs_after = inner.seen_pairs.len();
let names_after = inner.seen_names.len();
log::debug!(
target: "qpack_metrics",
"observer fold: contributed pairs={} names={} | shared seen_pairs {pairs_before}->{pairs_after} \
seen_names {names_before}->{names_after}",
accum.seen_pairs.len(),
accum.seen_names.len(),
);
}
pub(in crate::headers) fn is_hot(
&self,
name: &EntryName<'_>,
value: Option<&FieldLineValue<'_>>,
) -> bool {
let Some(key) = name.name_key() else {
return false;
};
let Ok(inner) = self.inner.lock() else {
return false;
};
match value {
Some(FieldLineValue::Static(s)) => inner.seen_pairs.contains(&(key, *s)),
_ => inner.seen_names.contains(&key),
}
}
pub(in crate::headers) fn prime(
&self,
capacity: u32,
compression: HeaderCompression,
) -> Vec<PrimingCandidate> {
if capacity == 0 {
return Vec::new();
}
let Ok(inner) = self.inner.lock() else {
return Vec::new();
};
let observed_pairs = inner.seen_pairs.len();
let observed_names = inner.seen_names.len();
let mut ranked: Vec<RankedCandidate> = Vec::new();
for &(key, s) in &inner.seen_pairs {
let name = key.into_entry_name();
let value = FieldLineValue::Static(s);
push_candidate(&mut ranked, name, Some(value), compression);
}
for &key in &inner.seen_names {
let name = key.into_entry_name();
push_candidate(&mut ranked, name, None, compression);
}
let ranked_total = ranked.len();
ranked.sort_by(|a, b| {
b.savings_per_ref
.cmp(&a.savings_per_ref)
.then_with(|| a.entry_size.cmp(&b.entry_size))
});
let mut out: Vec<PrimingCandidate> = Vec::new();
let mut used: u32 = 0;
let mut dropped_no_room = 0usize;
for c in ranked {
match used.checked_add(c.entry_size) {
Some(next) if next <= capacity => {
used = next;
if log::log_enabled!(target: "qpack_metrics", log::Level::Trace) {
log::trace!(
target: "qpack_metrics",
" primed [{idx}]: savings/ref={savings} entry_size={size} name={name:?} value={value}",
idx = out.len(),
savings = c.savings_per_ref,
size = c.entry_size,
name = c.name,
value = match &c.value {
Some(v) => format!("{:?}", String::from_utf8_lossy(v.as_bytes())),
None => "<name-only>".to_string(),
},
);
}
out.push(PrimingCandidate {
name: c.name,
value: c.value,
});
}
_ => {
dropped_no_room += 1;
}
}
}
log::debug!(
target: "qpack_metrics",
"observer prime(capacity={capacity}, {compression:?}): observed pairs={observed_pairs} names={observed_names} \
cost-passing={ranked_total} packed={} dropped_no_room={dropped_no_room} bytes_used={used}/{capacity}",
out.len(),
);
out
}
}
fn push_candidate(
ranked: &mut Vec<RankedCandidate>,
name: EntryName<'static>,
value: Option<FieldLineValue<'static>>,
compression: HeaderCompression,
) {
let Some(model) = CostModel::estimate(compression, &name, value.as_ref()) else {
return;
};
let value_len = value.as_ref().map_or(0, |v| v.as_bytes().len());
let entry_size = ENTRY_OVERHEAD
.saturating_add(u32::try_from(name.len()).unwrap_or(u32::MAX))
.saturating_add(u32::try_from(value_len).unwrap_or(u32::MAX));
ranked.push(RankedCandidate {
name,
value,
entry_size,
savings_per_ref: model.savings_per_ref,
});
}
#[derive(Default)]
pub(crate) struct ConnectionAccumulator {
seen_pairs: SmallVec<[(NameKey, &'static [u8]); 16]>,
high_card_names: SmallVec<[NameKey; 4]>,
seen_names: SmallVec<[NameKey; 32]>,
}
impl Debug for ConnectionAccumulator {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ConnectionAccumulator")
.field(
"seen_pairs",
&fmt::from_fn(|f| {
let mut f = f.debug_map();
for (name, value) in &self.seen_pairs {
f.entry(name, &format_args!("{}", String::from_utf8_lossy(value)));
}
f.finish()
}),
)
.field("high_card_names", &self.high_card_names)
.field("seen_names", &self.seen_names)
.finish()
}
}
impl ConnectionAccumulator {
pub(in crate::headers) fn observe(&mut self, name: &EntryName<'_>, value: &FieldLineValue<'_>) {
let Some(key) = name.name_key() else {
return;
};
let static_value = if name.has_uncacheable_value() {
None
} else {
match value {
FieldLineValue::Static(s) => Some(*s),
_ => None,
}
};
self.record(key, static_value);
}
pub(in crate::headers) fn record(&mut self, key: NameKey, static_value: Option<&'static [u8]>) {
if !self.seen_names.contains(&key) {
self.seen_names.push(key);
}
let Some(s) = static_value else {
return;
};
if self.high_card_names.contains(&key) {
return;
}
let mut same_pos: Option<usize> = None;
let mut diff_pos: Option<usize> = None;
for (i, (kk, ss)) in self.seen_pairs.iter().enumerate() {
if *kk != key {
continue;
}
if *ss == s {
same_pos = Some(i);
break;
}
diff_pos = Some(i);
}
match (same_pos, diff_pos) {
(Some(_), _) => {} (None, Some(i)) => {
self.seen_pairs.swap_remove(i);
self.high_card_names.push(key);
}
(None, None) => {
self.seen_pairs.push((key, s));
}
}
}
}
struct CostModel {
savings_per_ref: u32,
}
impl CostModel {
#[allow(clippy::match_same_arms)]
fn estimate(
compression: HeaderCompression,
name: &EntryName<'_>,
value: Option<&FieldLineValue<'_>>,
) -> Option<Self> {
let name_len = u32::try_from(name.len()).unwrap_or(u32::MAX);
let value_bytes = value.map(FieldLineValue::as_bytes);
let lookup = static_lookup(compression, name, value_bytes);
match (value, lookup) {
(Some(_), StaticHit::Full(_)) => None,
(Some(v), StaticHit::Name(_)) => {
let value_len = u32::try_from(v.len()).unwrap_or(u32::MAX);
Some(Self {
savings_per_ref: value_len,
})
}
(Some(v), StaticHit::None) => {
let value_len = u32::try_from(v.len()).unwrap_or(u32::MAX);
let overhead = match compression {
HeaderCompression::Qpack => 1,
HeaderCompression::Hpack => 2,
};
Some(Self {
savings_per_ref: name_len.saturating_add(value_len).saturating_add(overhead),
})
}
(None, StaticHit::Full(_) | StaticHit::Name(_)) => None,
(None, StaticHit::None) => Some(Self {
savings_per_ref: name_len,
}),
}
}
}
fn static_lookup(
compression: HeaderCompression,
name: &EntryName<'_>,
value: Option<&[u8]>,
) -> StaticHit {
match compression {
HeaderCompression::Qpack => {
crate::headers::qpack::static_table::static_table_lookup(name, value)
}
HeaderCompression::Hpack => {
crate::headers::hpack::static_table::static_table_lookup(name, value.unwrap_or(b""))
}
}
}
#[derive(Debug)]
pub(in crate::headers) struct PrimingCandidate {
pub(in crate::headers) name: EntryName<'static>,
pub(in crate::headers) value: Option<FieldLineValue<'static>>,
}
struct RankedCandidate {
name: EntryName<'static>,
value: Option<FieldLineValue<'static>>,
entry_size: u32,
savings_per_ref: u32,
}
#[cfg(test)]
mod tests {
use super::*;
fn name(known: KnownHeaderName) -> EntryName<'static> {
EntryName::Known(known)
}
fn value(bytes: &'static [u8]) -> FieldLineValue<'static> {
FieldLineValue::Static(bytes)
}
fn observe_once(
observer: &HeaderObserver,
pairs: &[(EntryName<'static>, FieldLineValue<'static>)],
) {
let mut accum = ConnectionAccumulator::default();
for (n, v) in pairs {
accum.observe(n, v);
}
observer.fold_connection(&accum);
}
#[test]
fn prime_emits_observed_pair_after_one_connection() {
let observer = HeaderObserver::default();
observe_once(
&observer,
&[(name(KnownHeaderName::Server), value(b"trillium"))],
);
let primed = observer.prime(4096, HeaderCompression::Qpack);
assert_eq!(primed.len(), 1, "expected 1 candidate, got {primed:?}");
assert_eq!(primed[0].name, name(KnownHeaderName::Server));
assert_eq!(primed[0].value, Some(value(b"trillium")));
}
#[test]
fn prime_skips_full_static_match() {
let observer = HeaderObserver::default();
observe_once(
&observer,
&[(EntryName::Pseudo(PseudoHeaderName::Status), value(b"200"))],
);
let primed = observer.prime(4096, HeaderCompression::Qpack);
assert!(
!primed.iter().any(
|c| matches!(c.name, EntryName::Pseudo(PseudoHeaderName::Status))
&& c.value.is_some()
),
"(:status, 200) should not prime; got {primed:?}",
);
}
#[test]
fn prime_ranks_by_savings_per_ref() {
let observer = HeaderObserver::default();
let big = (
name(KnownHeaderName::ContentType),
value(b"application/json; charset=utf-8"),
);
let small = (name(KnownHeaderName::ContentLength), value(b"12"));
observe_once(&observer, &[big.clone(), small.clone()]);
let primed = observer.prime(75, HeaderCompression::Qpack);
assert_eq!(primed.len(), 1);
assert_eq!(primed[0].name, big.0);
assert_eq!(primed[0].value, Some(big.1));
}
#[test]
fn high_cardinality_name_falls_back_to_name_only() {
let mut accum = ConnectionAccumulator::default();
accum.observe(&name(KnownHeaderName::Trailer), &value(b"value-a"));
accum.observe(&name(KnownHeaderName::Trailer), &value(b"value-b"));
let key = NameKey::Known(KnownHeaderName::Trailer);
assert!(accum.high_card_names.contains(&key));
assert!(!accum.seen_pairs.iter().any(|(k, _)| *k == key));
assert!(accum.seen_names.contains(&key));
}
#[test]
fn unknown_names_are_ignored() {
let observer = HeaderObserver::default();
let unknown: EntryName<'static> = EntryName::try_from(b"x-custom".to_vec()).unwrap();
let mut accum = ConnectionAccumulator::default();
accum.observe(&unknown, &value(b"hello"));
assert!(accum.seen_pairs.is_empty());
assert!(accum.seen_names.is_empty());
observer.fold_connection(&accum);
assert!(observer.prime(4096, HeaderCompression::Qpack).is_empty());
}
#[test]
fn unknown_static_is_tracked() {
let observer = HeaderObserver::default();
let unknown_static = EntryName::UnknownStatic("x-trillium-flag");
observe_once(&observer, &[(unknown_static.clone(), value(b"on"))]);
let primed = observer.prime(4096, HeaderCompression::Qpack);
assert!(
primed
.iter()
.any(|c| c.name == unknown_static && c.value == Some(value(b"on"))),
"UnknownStatic full-pair must prime; got {primed:?}",
);
}
#[test]
fn observe_skips_uncacheable_names() {
let mut accum = ConnectionAccumulator::default();
accum.observe(
&name(KnownHeaderName::Authorization),
&value(b"Bearer secret"),
);
assert!(accum.seen_pairs.is_empty());
let key = NameKey::Known(KnownHeaderName::Authorization);
assert!(accum.seen_names.contains(&key));
}
#[test]
fn observe_skips_non_static_values() {
let mut accum = ConnectionAccumulator::default();
accum.observe(
&name(KnownHeaderName::Server),
&FieldLineValue::Owned(b"trillium".to_vec()),
);
assert!(accum.seen_pairs.is_empty());
let key = NameKey::Known(KnownHeaderName::Server);
assert!(accum.seen_names.contains(&key));
}
#[test]
fn fold_is_set_union() {
let observer = HeaderObserver::default();
let pair_a = (name(KnownHeaderName::Server), value(b"trillium"));
let pair_b = (name(KnownHeaderName::UserAgent), value(b"test-agent/1.0"));
observe_once(&observer, std::slice::from_ref(&pair_a));
observe_once(&observer, std::slice::from_ref(&pair_b));
let primed = observer.prime(4096, HeaderCompression::Qpack);
assert!(
primed
.iter()
.any(|c| c.name == pair_a.0 && c.value.as_ref() == Some(&pair_a.1))
);
assert!(
primed
.iter()
.any(|c| c.name == pair_b.0 && c.value.as_ref() == Some(&pair_b.1))
);
}
#[test]
fn hpack_prime_emits_observed_pair() {
let observer = HeaderObserver::default();
observe_once(
&observer,
&[(name(KnownHeaderName::Server), value(b"trillium"))],
);
let primed = observer.prime(4096, HeaderCompression::Hpack);
assert_eq!(primed.len(), 1);
assert_eq!(primed[0].name, name(KnownHeaderName::Server));
assert_eq!(primed[0].value, Some(value(b"trillium")));
}
}