1use std::collections::HashMap;
8
9use crate::event::Event;
10use crate::slug::Stage;
11use crate::token::Token;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub struct StageId(pub u16);
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub struct TokenId(pub u32);
20
21#[derive(Debug, Default)]
24pub struct InternTables {
25 stages: Vec<Stage>,
26 stage_index: HashMap<Stage, StageId>,
27 tokens: Vec<Token>,
28 token_index: HashMap<Token, TokenId>,
29}
30
31impl InternTables {
32 pub fn stage(&mut self, stage: &Stage) -> StageId {
34 if let Some(id) = self.stage_index.get(stage) {
35 return *id;
36 }
37 #[allow(clippy::cast_possible_truncation)] let id = StageId(self.stages.len() as u16);
39 self.stages.push(stage.clone());
40 self.stage_index.insert(stage.clone(), id);
41 id
42 }
43
44 pub fn token(&mut self, token: Token) -> TokenId {
46 if let Some(id) = self.token_index.get(&token) {
47 return *id;
48 }
49 #[allow(clippy::cast_possible_truncation)] let id = TokenId(self.tokens.len() as u32);
51 self.tokens.push(token);
52 self.token_index.insert(token, id);
53 id
54 }
55
56 #[must_use]
58 pub fn stage_name(&self, id: StageId) -> Option<&Stage> {
59 self.stages.get(id.0 as usize)
60 }
61
62 #[must_use]
64 pub fn token_id(&self, token: Token) -> Option<TokenId> {
65 self.token_index.get(&token).copied()
66 }
67}
68
69const NO_VARIANT: u8 = u8::MAX;
71
72#[derive(Debug, Default)]
76pub struct EventLog {
77 token_ids: Vec<TokenId>,
78 stage_ids: Vec<StageId>,
79 actors: Vec<u8>,
80 variants: Vec<u8>,
81 at_ms: Vec<u64>,
82 seqs: Vec<u32>,
83}
84
85impl EventLog {
86 pub fn push(&mut self, event: &Event, tables: &mut InternTables) {
88 self.token_ids.push(tables.token(event.token));
89 self.stage_ids.push(tables.stage(&event.stage));
90 self.actors.push(event.actor.code());
91 self.variants.push(event.variant.unwrap_or(NO_VARIANT));
92 self.at_ms.push(event.at.as_unix_ms());
93 self.seqs.push(event.seq.0);
94 }
95
96 #[must_use]
98 pub fn len(&self) -> usize {
99 self.token_ids.len()
100 }
101
102 #[must_use]
104 pub fn is_empty(&self) -> bool {
105 self.token_ids.is_empty()
106 }
107
108 #[must_use]
111 pub fn stage_counts(&self, token: TokenId, tables: &InternTables) -> Vec<(Stage, u64)> {
112 let mut counts = vec![0u64; tables.stages.len()];
113 for (t, s) in self.token_ids.iter().zip(&self.stage_ids) {
114 if *t == token {
115 counts[s.0 as usize] += 1;
116 }
117 }
118 counts
119 .into_iter()
120 .enumerate()
121 .filter(|(_, c)| *c > 0)
122 .filter_map(|(i, c)| {
123 #[allow(clippy::cast_possible_truncation)] tables.stage_name(StageId(i as u16)).map(|s| (s.clone(), c))
125 })
126 .collect()
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use crate::event::{ActorClass, Seq};
134 use crate::{ResolverContext, Timestamp};
135
136 fn ev(token: Token, stage: Stage, seq: u32) -> Event {
137 Event {
138 token,
139 stage,
140 actor: ActorClass::from_context(&ResolverContext::human()),
141 at: Timestamp::from_unix_ms(u64::from(seq)),
142 seq: Seq(seq),
143 variant: if seq % 2 == 0 { Some(1) } else { None },
144 }
145 }
146
147 #[test]
148 fn interning_is_idempotent_and_dense() {
149 let mut tables = InternTables::default();
150 let a = tables.stage(&Stage::resolve());
151 let b = tables.stage(&Stage::resolve());
152 assert_eq!(a, b);
153 let c = tables.stage(&Stage::run());
154 assert_eq!(c.0, a.0 + 1, "dense, append-only ids");
155 }
156
157 #[test]
158 fn soa_counts_agree_with_naive_counting() {
159 let mut tables = InternTables::default();
160 let mut log = EventLog::default();
161 let t1 = Token::parse("one").unwrap();
162 let t2 = Token::parse("two").unwrap();
163 for i in 0..10 {
164 log.push(&ev(t1, Stage::resolve(), i), &mut tables);
165 }
166 for i in 0..3 {
167 log.push(&ev(t2, Stage::run(), i), &mut tables);
168 log.push(&ev(t1, Stage::run(), 100 + i), &mut tables);
169 }
170 let id1 = tables.token_id(t1).unwrap();
171 let counts = log.stage_counts(id1, &tables);
172 assert!(counts.contains(&(Stage::resolve(), 10)));
173 assert!(counts.contains(&(Stage::run(), 3)));
174 assert_eq!(log.len(), 16);
175 }
176}