use std::collections::{BTreeMap, HashSet};
use crate::PropertyType;
use crate::content::node::NodeState;
use crate::error::{Error, Result};
use crate::index::counter::sip_hash::SipHash;
use crate::index::definition::IndexDefinition;
use crate::segment::record::RecordIdentifier;
use crate::writer::record_writer::{
ChildNodesToWrite, PropertyToWrite, PropertyValuesToWrite, RecordWriter, SegmentSink,
};
const COUNT_PROPERTY_NAME: &str = ":cnt";
const DEFAULT_RESOLUTION: i64 = 1000;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct HitCount {
pub hits: u64,
pub nodes_visited: u64,
pub credited_nodes: usize,
}
pub struct BuiltCounter {
pub index_record: Option<RecordIdentifier>,
pub created_seed: Option<i64>,
pub credited_by_path: BTreeMap<String, i64>,
}
pub struct CounterBuilder {
seed: i64,
created_seed: Option<i64>,
bit_mask: i32,
}
impl CounterBuilder {
#[must_use]
pub fn new(definition: &IndexDefinition) -> Self {
let resolution = definition.resolution.unwrap_or(DEFAULT_RESOLUTION);
let bit_mask = bit_mask_for(resolution);
if let Some(stored) = definition.seed {
return Self {
seed: i64::from(stored as i32),
created_seed: None,
bit_mask,
};
}
let drawn = drawn_seed();
Self {
seed: drawn,
created_seed: Some(drawn),
bit_mask,
}
}
#[must_use]
pub fn seed(&self) -> i64 {
self.seed
}
#[must_use]
pub fn bit_mask(&self) -> i32 {
self.bit_mask
}
pub fn count_hits(&self, state_root: &NodeState<'_>) -> Result<HitCount> {
let credited = self.accumulate(state_root)?;
Ok(HitCount {
hits: credited.hits,
nodes_visited: credited.nodes_visited,
credited_nodes: credited.by_path.len(),
})
}
pub fn build<Sink: SegmentSink>(
&self,
state_root: &NodeState<'_>,
writer: &mut RecordWriter<Sink>,
) -> Result<BuiltCounter> {
let accumulated = self.accumulate(state_root)?;
if accumulated.by_path.is_empty() {
return Ok(BuiltCounter {
index_record: None,
created_seed: self.created_seed,
credited_by_path: BTreeMap::new(),
});
}
let index_record = write_counter_tree(writer, &accumulated.by_path)?;
Ok(BuiltCounter {
index_record: Some(index_record),
created_seed: self.created_seed,
credited_by_path: accumulated.by_path,
})
}
fn accumulate(&self, state_root: &NodeState<'_>) -> Result<Accumulated> {
let increment = i64::from(self.bit_mask) + 1;
let mut accumulated = Accumulated::default();
let mut stack = vec![CounterStep::Visit {
node: *state_root,
path: String::new(),
hash: SipHash::seeded(self.seed),
}];
let mut ancestors: HashSet<RecordIdentifier> = HashSet::new();
while let Some(step) = stack.pop() {
match step {
CounterStep::Leave { record } => {
ancestors.remove(&record);
}
CounterStep::Visit { node, path, hash } => {
let record = node.record_identifier();
if !ancestors.insert(record) {
return Err(Error::InvalidFormat {
details: format!(
"the node at {} is its own ancestor, so the counter cannot \
be rebuilt from it",
if path.is_empty() { "/" } else { &path }
),
});
}
stack.push(CounterStep::Leave { record });
accumulated.nodes_visited += 1;
let mut entries = node.child_node_entries()?;
entries.sort_by(|left, right| left.0.as_bytes().cmp(right.0.as_bytes()));
for (name, child) in entries.into_iter().rev() {
if name.starts_with(':') {
continue;
}
let child_hash = hash.for_child(&name);
let child_path = format!("{path}/{name}");
if child_hash.hash_code() & self.bit_mask == 0 {
accumulated.hits += 1;
accumulated.credit(&path, increment);
}
stack.push(CounterStep::Visit {
node: child,
path: child_path,
hash: child_hash,
});
}
}
}
}
Ok(accumulated)
}
}
enum CounterStep<'provider> {
Visit {
node: NodeState<'provider>,
path: String,
hash: SipHash,
},
Leave {
record: RecordIdentifier,
},
}
#[derive(Default)]
struct Accumulated {
by_path: BTreeMap<String, i64>,
hits: u64,
nodes_visited: u64,
}
impl Accumulated {
fn credit(&mut self, path: &str, increment: i64) {
let mut current = path;
loop {
let key = if current.is_empty() { "/" } else { current };
*self.by_path.entry(key.to_owned()).or_insert(0) += increment;
if current.is_empty() {
break;
}
current = match current.rfind('/') {
Some(position) => ¤t[..position],
None => "",
};
}
}
}
fn write_counter_tree<Sink: SegmentSink>(
writer: &mut RecordWriter<Sink>,
credited: &BTreeMap<String, i64>,
) -> Result<RecordIdentifier> {
let mut paths: Vec<&String> = credited.keys().collect();
paths.sort_by_key(|path| std::cmp::Reverse(path_depth(path)));
let mut written: BTreeMap<String, RecordIdentifier> = BTreeMap::new();
let mut children_of: BTreeMap<String, Vec<(String, RecordIdentifier)>> = BTreeMap::new();
for path in paths {
let count = credited.get(path).copied().unwrap_or(0);
let value = writer.write_string(&count.to_string())?;
let mut children = children_of.remove(path.as_str()).unwrap_or_default();
children.sort_by(|left, right| left.0.as_bytes().cmp(right.0.as_bytes()));
let record = writer.write_node(
None,
&[],
&match children.as_slice() {
[] => ChildNodesToWrite::Zero,
[(name, node)] => ChildNodesToWrite::One {
name: name.clone(),
node: *node,
},
many => ChildNodesToWrite::Many(many.to_vec()),
},
&[PropertyToWrite {
name: COUNT_PROPERTY_NAME.to_owned(),
property_type: PropertyType::Long,
values: PropertyValuesToWrite::Single(value),
}],
)?;
written.insert(path.clone(), record);
if path != "/" {
let (parent, name) = split_parent(path);
children_of.entry(parent).or_default().push((name, record));
}
}
written
.get("/")
.copied()
.ok_or_else(|| Error::InvalidFormat {
details: "the counter accumulation credited no root, which cannot happen when any \
node hit"
.to_owned(),
})
}
fn path_depth(path: &str) -> usize {
path.split('/')
.filter(|element| !element.is_empty())
.count()
}
fn split_parent(path: &str) -> (String, String) {
match path.rfind('/') {
Some(0) => ("/".to_owned(), path[1..].to_owned()),
Some(position) => (path[..position].to_owned(), path[position + 1..].to_owned()),
None => ("/".to_owned(), path.to_owned()),
}
}
fn bit_mask_for(resolution: i64) -> i32 {
let resolution = i32::try_from(resolution).unwrap_or(i32::MAX);
if resolution <= 0 {
return 0;
}
let highest = 1i32 << (31 - resolution.leading_zeros());
highest.wrapping_mul(2).wrapping_sub(1)
}
fn drawn_seed() -> i64 {
i64::from(crate::writer::identifier_generator::random_u32() as i32)
}