use crate::{
KnownHeaderName,
headers::{
entry_name::{EntryName, PseudoHeaderName},
field_section::FieldLineValue,
qpack,
static_hit::StaticHit,
},
};
use hashbrown::HashSet;
use smallvec::SmallVec;
use std::{
fmt::{self, Debug},
sync::Mutex,
};
#[cfg(test)]
mod tests;
const ENTRY_OVERHEAD: u32 = 32;
#[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!(
"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) -> 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));
}
for &key in &inner.seen_names {
let name = key.into_entry_name();
push_candidate(&mut ranked, name, None);
}
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;
log::trace!(
"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!(
"observer prime(capacity={capacity}): 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>>,
) {
let Some(model) = CostModel::estimate(&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,
reason = "arms differ semantically (None vs StaticHit::Full/Name) and are kept separate \
for clarity"
)]
fn estimate(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 = qpack::static_table::static_table_lookup(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);
Some(Self {
savings_per_ref: name_len.saturating_add(value_len).saturating_add(1),
})
}
(None, StaticHit::Full(_) | StaticHit::Name(_)) => None,
(None, StaticHit::None) => Some(Self {
savings_per_ref: name_len,
}),
}
}
}
#[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,
}