use std::collections::HashMap;
use crate::event::Event;
use crate::slug::Stage;
use crate::token::Token;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct StageId(pub u16);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TokenId(pub u32);
#[derive(Debug, Default)]
pub struct InternTables {
stages: Vec<Stage>,
stage_index: HashMap<Stage, StageId>,
tokens: Vec<Token>,
token_index: HashMap<Token, TokenId>,
}
impl InternTables {
pub fn stage(&mut self, stage: &Stage) -> StageId {
if let Some(id) = self.stage_index.get(stage) {
return *id;
}
#[allow(clippy::cast_possible_truncation)] let id = StageId(self.stages.len() as u16);
self.stages.push(stage.clone());
self.stage_index.insert(stage.clone(), id);
id
}
pub fn token(&mut self, token: Token) -> TokenId {
if let Some(id) = self.token_index.get(&token) {
return *id;
}
#[allow(clippy::cast_possible_truncation)] let id = TokenId(self.tokens.len() as u32);
self.tokens.push(token);
self.token_index.insert(token, id);
id
}
#[must_use]
pub fn stage_name(&self, id: StageId) -> Option<&Stage> {
self.stages.get(id.0 as usize)
}
#[must_use]
pub fn token_id(&self, token: Token) -> Option<TokenId> {
self.token_index.get(&token).copied()
}
}
const NO_VARIANT: u8 = u8::MAX;
#[derive(Debug, Default)]
pub struct EventLog {
token_ids: Vec<TokenId>,
stage_ids: Vec<StageId>,
actors: Vec<u8>,
variants: Vec<u8>,
regions: Vec<u8>,
at_ms: Vec<u64>,
seqs: Vec<u32>,
}
impl EventLog {
pub fn push(&mut self, event: &Event, tables: &mut InternTables) {
self.token_ids.push(tables.token(event.token));
self.stage_ids.push(tables.stage(&event.stage));
self.actors.push(event.actor.code());
self.variants.push(event.variant.unwrap_or(NO_VARIANT));
self.regions.push(event.regions.unwrap_or(0));
self.at_ms.push(event.at.as_unix_ms());
self.seqs.push(event.seq.0);
}
#[must_use]
pub fn len(&self) -> usize {
self.token_ids.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.token_ids.is_empty()
}
#[must_use]
pub fn stage_counts(&self, token: TokenId, tables: &InternTables) -> Vec<(Stage, u64)> {
let mut counts = vec![0u64; tables.stages.len()];
for (t, s) in self.token_ids.iter().zip(&self.stage_ids) {
if *t == token {
counts[s.0 as usize] += 1;
}
}
counts
.into_iter()
.enumerate()
.filter(|(_, c)| *c > 0)
.filter_map(|(i, c)| {
#[allow(clippy::cast_possible_truncation)] tables.stage_name(StageId(i as u16)).map(|s| (s.clone(), c))
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::{ActorClass, Seq};
use crate::{ResolverContext, Timestamp};
fn ev(token: Token, stage: Stage, seq: u32) -> Event {
Event {
token,
stage,
actor: ActorClass::from_context(&ResolverContext::human()),
at: Timestamp::from_unix_ms(u64::from(seq)),
seq: Seq(seq),
variant: if seq % 2 == 0 { Some(1) } else { None },
regions: None,
}
}
#[test]
fn interning_is_idempotent_and_dense() {
let mut tables = InternTables::default();
let a = tables.stage(&Stage::resolve());
let b = tables.stage(&Stage::resolve());
assert_eq!(a, b);
let c = tables.stage(&Stage::run());
assert_eq!(c.0, a.0 + 1, "dense, append-only ids");
}
#[test]
fn soa_counts_agree_with_naive_counting() {
let mut tables = InternTables::default();
let mut log = EventLog::default();
let t1 = Token::parse("one").unwrap();
let t2 = Token::parse("two").unwrap();
for i in 0..10 {
log.push(&ev(t1, Stage::resolve(), i), &mut tables);
}
for i in 0..3 {
log.push(&ev(t2, Stage::run(), i), &mut tables);
log.push(&ev(t1, Stage::run(), 100 + i), &mut tables);
}
let id1 = tables.token_id(t1).unwrap();
let counts = log.stage_counts(id1, &tables);
assert!(counts.contains(&(Stage::resolve(), 10)));
assert!(counts.contains(&(Stage::run(), 3)));
assert_eq!(log.len(), 16);
}
}