1use std::collections::VecDeque;
4use std::sync::{Arc, Condvar, Mutex, OnceLock};
5
6use rustc_hash::FxHashMap;
7
8use super::slice::TokenSliceCatalog;
9use super::trie::VocabTrie;
10use crate::error::{CompileError, Stage};
11use crate::mem_gate::{acquire_vocab_build_bytes, BuildGate, PermitGuard};
12use crate::vocab::{
13 projected_peak_bytes, try_alloc, CanonicalVocabulary, PreparedVocabulary, Vocabulary,
14};
15
16#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
19pub struct VocabFingerprint([u8; 32]);
20
21impl VocabFingerprint {
22 pub fn of(vocab: &Vocabulary) -> Result<Self, CompileError> {
25 match PreparedVocabulary::build(vocab) {
26 Ok(prepared) => Ok(Self::of_prepared(&prepared, vocab.eos_token_id())),
27 Err(_) => Self::try_of_unprepared(vocab),
28 }
29 }
30
31 pub(crate) fn of_prepared(prepared: &PreparedVocabulary, eos: u32) -> Self {
32 Self::of_prepared_with_ordinary_max(prepared, eos).0
33 }
34
35 pub(crate) fn of_prepared_with_ordinary_max(
38 prepared: &PreparedVocabulary,
39 eos: u32,
40 ) -> (Self, Option<u32>) {
41 let mut hasher = blake3::Hasher::new();
42 let mut ordinary_max = None;
43 for (bytes, ids) in prepared.iter() {
44 hasher.update(&(bytes.len() as u64).to_le_bytes());
45 hasher.update(bytes);
46 hasher.update(&(ids.len() as u64).to_le_bytes());
47 for &id in ids {
48 hasher.update(&id.to_le_bytes());
49 ordinary_max = Some(ordinary_max.map_or(id, |m: u32| m.max(id)));
50 }
51 }
52 hasher.update(&eos.to_le_bytes());
53 (Self(*hasher.finalize().as_bytes()), ordinary_max)
54 }
55
56 fn try_of_unprepared(vocab: &Vocabulary) -> Result<Self, CompileError> {
59 let mut entries: Vec<(&[u8], Vec<u32>)> =
60 try_alloc(vocab.tokens().len(), "fingerprint fallback entry scratch")?;
61 for (bytes, ids) in vocab.tokens() {
62 let mut sorted: Vec<u32> = try_alloc(ids.len(), "fingerprint fallback id scratch")?;
63 sorted.extend_from_slice(ids);
64 sorted.sort_unstable();
65 entries.push((bytes.as_slice(), sorted));
66 }
67 entries.sort_unstable_by(|a, b| a.0.cmp(b.0));
68 let mut hasher = blake3::Hasher::new();
69 for (bytes, ids) in &entries {
70 hasher.update(&(bytes.len() as u64).to_le_bytes());
71 hasher.update(bytes);
72 hasher.update(&(ids.len() as u64).to_le_bytes());
73 for &id in ids {
74 hasher.update(&id.to_le_bytes());
75 }
76 }
77 hasher.update(&vocab.eos_token_id().to_le_bytes());
78 Ok(Self(*hasher.finalize().as_bytes()))
79 }
80
81 #[cfg(test)]
83 pub(crate) const fn from_bytes(bytes: [u8; 32]) -> Self {
84 Self(bytes)
85 }
86
87 #[must_use]
89 pub fn as_bytes(&self) -> [u8; 32] {
90 self.0
91 }
92}
93
94const DEFAULT_TRIE_CACHE_BYTES: usize = 1 << 28;
97
98const CACHE_ENTRY_OVERHEAD: usize = 2 * std::mem::size_of::<usize>()
101 + std::mem::size_of::<VocabTrie>()
102 + std::mem::size_of::<VocabFingerprint>()
103 + std::mem::size_of::<usize>()
104 + 64;
105
106const DEFAULT_MAX_CONCURRENT_BUILDS: usize = 8;
109
110const DEFAULT_MAX_TRANSIENT_BUILD_BYTES: usize = 1 << 30;
113
114fn estimate_transient_build_bytes(vocab: &Vocabulary) -> usize {
117 let total_bytes = vocab
118 .tokens()
119 .keys()
120 .try_fold(0usize, |acc, k| acc.checked_add(k.len()))
121 .unwrap_or(usize::MAX);
122 let count = vocab.len();
123 total_bytes
124 .saturating_mul(48)
125 .saturating_add(count.saturating_mul(12))
126}
127
128fn estimate_transient_build_bytes_prepared(prepared: &PreparedVocabulary) -> usize {
131 prepared
132 .total_bytes()
133 .saturating_mul(48)
134 .saturating_add(prepared.total_ids().saturating_add(1).saturating_mul(12))
135}
136
137enum BuildSource<'a> {
140 Vocab(&'a Vocabulary),
141 Prepared(&'a PreparedVocabulary),
142}
143
144#[cfg(test)]
145use std::sync::atomic::{AtomicBool, Ordering};
146#[cfg(test)]
147static DELAY_BUILDS: AtomicBool = AtomicBool::new(false);
148#[cfg(test)]
149static DELAY_TEST_LOCK: Mutex<()> = Mutex::new(());
150
151#[cfg(test)]
152thread_local! {
153 static PANIC_ON_BUILD: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
154}
155
156#[derive(Debug)]
159struct BuildSlot {
160 result: Mutex<Option<Result<Arc<VocabTrie>, CompileError>>>,
161 ready: Condvar,
162}
163
164#[derive(Debug)]
166struct CacheEntry {
167 trie: Arc<VocabTrie>,
168 bytes: usize,
169 last_used: u64,
170}
171
172#[derive(Debug, Default)]
173struct CacheState {
174 map: FxHashMap<VocabFingerprint, CacheEntry>,
175 bytes: usize,
176 tick: u64,
177 lru: VecDeque<(VocabFingerprint, u64)>,
178 in_flight: FxHashMap<VocabFingerprint, Arc<BuildSlot>>,
179}
180
181impl CacheState {
182 fn touch(&mut self, fp: VocabFingerprint) -> u64 {
185 self.tick += 1;
186 let now = self.tick;
187 self.lru.push_back((fp, now));
188 if self.lru.len() > 4 * self.map.len().max(1) + 64 {
189 self.compact_lru();
190 }
191 now
192 }
193
194 fn compact_lru(&mut self) {
196 let map = &self.map;
197 self.lru
198 .retain(|(fp, tick)| map.get(fp).is_some_and(|e| e.last_used == *tick));
199 }
200
201 fn evict_until_fits(&mut self, need: usize, budget: usize, stats: &mut CacheStats) {
204 while self.bytes.saturating_add(need) > budget {
205 let Some((fp, tick)) = self.lru.pop_front() else {
206 break; };
208 if self.map.get(&fp).is_some_and(|e| e.last_used == tick) {
209 if let Some(e) = self.map.remove(&fp) {
210 self.bytes -= e.bytes;
211 stats.evictions += 1;
212 }
213 } }
215 }
216}
217
218#[derive(Debug, Default)]
220struct CacheStats {
221 hits: u64,
222 misses: u64,
223 evictions: u64,
224 oversized_bypasses: u64,
225}
226
227#[derive(Debug)]
230pub struct TrieCache {
231 state: Mutex<CacheState>,
232 stats: Mutex<CacheStats>,
233 budget: usize,
234 gate: BuildGate,
235}
236
237impl Default for TrieCache {
238 fn default() -> Self {
239 Self::with_build_limit(DEFAULT_TRIE_CACHE_BYTES, DEFAULT_MAX_CONCURRENT_BUILDS)
240 }
241}
242
243struct SlotGuard<'a> {
246 cache: &'a TrieCache,
247 fp: VocabFingerprint,
248 slot: Arc<BuildSlot>,
249 armed: bool,
250}
251
252impl Drop for SlotGuard<'_> {
253 fn drop(&mut self) {
254 if self.armed {
255 let mut r = self.slot.result.lock().unwrap_or_else(|p| p.into_inner());
256 if r.is_none() {
257 *r = Some(Err(CompileError::new(
258 crate::error::ErrorCode::InternalLimitExceeded,
259 crate::error::Stage::L4Bind,
260 "trie build failed to publish a result",
261 )));
262 }
263 drop(r);
264 self.slot.ready.notify_all();
265 self.cache
266 .state
267 .lock()
268 .unwrap_or_else(|p| p.into_inner())
269 .in_flight
270 .remove(&self.fp);
271 }
272 }
273}
274
275impl TrieCache {
276 #[must_use]
278 pub fn new() -> Self {
279 Self::default()
280 }
281
282 #[must_use]
284 pub fn with_budget(budget: usize) -> Self {
285 Self::with_build_limit(budget, DEFAULT_MAX_CONCURRENT_BUILDS)
286 }
287
288 #[must_use]
290 pub fn with_build_limit(budget: usize, max_concurrent_builds: usize) -> Self {
291 Self::with_build_gate(
292 budget,
293 max_concurrent_builds,
294 DEFAULT_MAX_TRANSIENT_BUILD_BYTES,
295 )
296 }
297
298 #[must_use]
301 pub fn with_build_gate(
302 budget: usize,
303 max_concurrent_builds: usize,
304 max_transient_build_bytes: usize,
305 ) -> Self {
306 Self {
307 state: Mutex::new(CacheState::default()),
308 stats: Mutex::new(CacheStats::default()),
309 budget,
310 gate: BuildGate::new(
311 max_concurrent_builds,
312 max_transient_build_bytes,
313 Stage::L4Bind,
314 ),
315 }
316 }
317
318 #[must_use]
320 pub fn peak_concurrent_builds(&self) -> usize {
321 self.gate.peak_concurrent()
322 }
323
324 #[must_use]
326 pub fn peak_concurrent_build_bytes(&self) -> usize {
327 self.gate.peak_bytes()
328 }
329
330 pub fn get_or_build_byte(&self, vocab: &Vocabulary) -> Result<Arc<VocabTrie>, CompileError> {
333 self.get_or_build_byte_prevalidated(vocab, VocabFingerprint::of(vocab)?)
334 }
335
336 pub(crate) fn get_or_build_byte_prevalidated(
339 &self,
340 vocab: &Vocabulary,
341 fingerprint: VocabFingerprint,
342 ) -> Result<Arc<VocabTrie>, CompileError> {
343 self.get_or_build_byte_impl(BuildSource::Vocab(vocab), fingerprint)
344 }
345
346 pub(crate) fn get_or_build_byte_from_prepared(
349 &self,
350 prepared: &PreparedVocabulary,
351 fingerprint: VocabFingerprint,
352 ) -> Result<Arc<VocabTrie>, CompileError> {
353 self.get_or_build_byte_impl(BuildSource::Prepared(prepared), fingerprint)
354 }
355
356 fn get_or_build_byte_impl(
358 &self,
359 source: BuildSource<'_>,
360 fingerprint: VocabFingerprint,
361 ) -> Result<Arc<VocabTrie>, CompileError> {
362 let slot = {
363 let mut state = self.state.lock().unwrap_or_else(|p| p.into_inner());
364 if state.map.contains_key(&fingerprint) {
365 let now = state.touch(fingerprint);
366 let entry = state
367 .map
368 .get_mut(&fingerprint)
369 .expect("just confirmed present under the same lock hold");
370 entry.last_used = now;
371 let trie = entry.trie.clone();
372 self.stats.lock().unwrap_or_else(|p| p.into_inner()).hits += 1;
373 return Ok(trie);
374 }
375 if let Some(slot) = state.in_flight.get(&fingerprint).cloned() {
376 drop(state);
377 let result = {
378 let mut r = slot.result.lock().unwrap_or_else(|p| p.into_inner());
379 while r.is_none() {
380 r = slot.ready.wait(r).unwrap_or_else(|p| p.into_inner());
381 }
382 r.clone().expect("slot is ready")
383 };
384 if result.is_ok() {
385 self.stats.lock().unwrap_or_else(|p| p.into_inner()).hits += 1;
386 }
387 return result;
388 }
389 let slot = Arc::new(BuildSlot {
390 result: Mutex::new(None),
391 ready: Condvar::new(),
392 });
393 state.in_flight.insert(fingerprint, slot.clone());
394 slot
395 };
396
397 let mut guard = SlotGuard {
398 cache: self,
399 fp: fingerprint,
400 slot: slot.clone(),
401 armed: true,
402 };
403 let built: Result<Arc<VocabTrie>, CompileError> = (|| {
404 let est = match source {
405 BuildSource::Vocab(v) => estimate_transient_build_bytes(v),
406 BuildSource::Prepared(p) => estimate_transient_build_bytes_prepared(p),
407 };
408 let _permit = self.gate.acquire(est)?;
409 #[cfg(test)]
410 if DELAY_BUILDS.load(Ordering::Relaxed) {
411 std::thread::sleep(std::time::Duration::from_millis(20));
412 }
413 #[cfg(test)]
414 if PANIC_ON_BUILD.with(|p| p.get()) {
415 panic!("injected test panic mid-build");
416 }
417 let trie = match source {
418 BuildSource::Vocab(v) => VocabTrie::build_byte_with_fingerprint(v, fingerprint)?,
419 BuildSource::Prepared(p) => VocabTrie::build_byte_from_prepared(p, fingerprint)?,
420 };
421 Ok(Arc::new(trie))
422 })();
423
424 {
425 let mut r = slot.result.lock().unwrap_or_else(|p| p.into_inner());
426 *r = Some(built.clone());
427 slot.ready.notify_all();
428 }
429 {
430 let mut state = self.state.lock().unwrap_or_else(|p| p.into_inner());
431 state.in_flight.remove(&fingerprint);
432 if let Ok(ref arc) = built {
433 let mut stats = self.stats.lock().unwrap_or_else(|p| p.into_inner());
434 stats.misses += 1;
435 let entry_bytes = arc.heap_bytes().saturating_add(CACHE_ENTRY_OVERHEAD);
436 if entry_bytes > self.budget {
437 stats.oversized_bypasses += 1; } else {
439 state.evict_until_fits(entry_bytes, self.budget, &mut stats);
440 let now = state.touch(fingerprint);
441 state.bytes += entry_bytes;
442 state.map.insert(
443 fingerprint,
444 CacheEntry {
445 trie: arc.clone(),
446 bytes: entry_bytes,
447 last_used: now,
448 },
449 );
450 }
451 }
452 }
453 guard.armed = false; built
455 }
456
457 pub fn bind(&self, handle: &VocabularyHandle) -> Result<BoundByteTrie, CompileError> {
460 let trie = self.get_or_build_byte_from_prepared(handle.prepared(), handle.fingerprint)?;
461 Ok(BoundByteTrie {
462 trie,
463 fingerprint: handle.fingerprint,
464 })
465 }
466
467 pub fn clear(&self) {
470 let mut state = self.state.lock().unwrap_or_else(|p| p.into_inner());
471 state.map.clear();
472 state.bytes = 0;
473 state.lru.clear();
474 state.tick = 0;
475 }
476
477 #[must_use]
479 pub fn len(&self) -> usize {
480 self.state
481 .lock()
482 .unwrap_or_else(|p| p.into_inner())
483 .map
484 .len()
485 }
486
487 #[must_use]
489 pub fn is_empty(&self) -> bool {
490 self.len() == 0
491 }
492
493 #[must_use]
496 pub fn bytes(&self) -> usize {
497 self.state.lock().unwrap_or_else(|p| p.into_inner()).bytes
498 }
499
500 #[must_use]
503 pub fn stats(&self) -> (u64, u64, u64, u64) {
504 let s = self.stats.lock().unwrap_or_else(|p| p.into_inner());
505 (s.hits, s.misses, s.evictions, s.oversized_bypasses)
506 }
507}
508
509#[derive(Clone, Debug)]
512pub struct VocabularyHandle {
513 canonical: Arc<CanonicalVocabulary>,
514 fingerprint: VocabFingerprint,
515 token_slices: Arc<OnceLock<Option<Arc<TokenSliceCatalog>>>>,
516}
517
518const PAIR_SCRATCH_BYTES_PER_TOKEN: usize = 4 * std::mem::size_of::<usize>();
521
522const TUPLE_DESCRIPTOR_BYTES_PER_TOKEN: usize = 2 * 3 * std::mem::size_of::<usize>();
524
525const HASHMAP_SLOT_BYTES_PER_TOKEN: usize = std::mem::size_of::<(Vec<u8>, Vec<u32>)>() + 16;
528
529#[must_use]
532pub(crate) fn handle_construction_peak_bytes_from_totals(
533 total_bytes: usize,
534 total_ids: usize,
535 token_count: usize,
536) -> usize {
537 let ids_bytes = total_ids.saturating_mul(4);
538 let descriptors = token_count.saturating_mul(TUPLE_DESCRIPTOR_BYTES_PER_TOKEN);
539 let hashmap_buckets = token_count.saturating_mul(HASHMAP_SLOT_BYTES_PER_TOKEN);
540
541 let copy_peak = descriptors
542 .saturating_add(total_bytes)
543 .saturating_add(ids_bytes);
544 let map_peak = copy_peak.saturating_add(hashmap_buckets);
545
546 let source_map_retained = total_bytes
547 .saturating_add(ids_bytes)
548 .saturating_add(hashmap_buckets);
549 let canonical_build = projected_peak_bytes(total_bytes, total_ids, token_count)
550 .saturating_add(token_count.saturating_mul(PAIR_SCRATCH_BYTES_PER_TOKEN));
551 let canonical_peak = source_map_retained.saturating_add(canonical_build);
552
553 copy_peak.max(map_peak).max(canonical_peak)
554}
555
556fn handle_construction_peak_bytes(vocab: &Vocabulary) -> usize {
558 let total_bytes = vocab
559 .tokens()
560 .keys()
561 .try_fold(0usize, |acc, k| acc.checked_add(k.len()))
562 .unwrap_or(usize::MAX);
563 let total_ids = vocab
564 .tokens()
565 .values()
566 .try_fold(0usize, |acc, v| acc.checked_add(v.len()))
567 .unwrap_or(usize::MAX);
568 handle_construction_peak_bytes_from_totals(total_bytes, total_ids, vocab.tokens().len())
569}
570
571impl VocabularyHandle {
572 pub fn new(vocab: Arc<Vocabulary>) -> Result<Self, crate::error::CompileError> {
575 Self::new_with_logits_vocab_size(vocab, None)
576 }
577
578 pub fn new_with_logits_vocab_size(
581 vocab: Arc<Vocabulary>,
582 logits_vocab_size: Option<usize>,
583 ) -> Result<Self, crate::error::CompileError> {
584 let permit = acquire_vocab_build_bytes(handle_construction_peak_bytes(&vocab))?;
585 Self::new_admitted_with_logits_vocab_size(vocab, logits_vocab_size, &permit)
586 }
587
588 pub fn new_admitted(
591 vocab: Arc<Vocabulary>,
592 permit: &PermitGuard<'_>,
593 ) -> Result<Self, crate::error::CompileError> {
594 Self::new_admitted_with_logits_vocab_size(vocab, None, permit)
595 }
596
597 pub fn new_admitted_with_logits_vocab_size(
599 vocab: Arc<Vocabulary>,
600 logits_vocab_size: Option<usize>,
601 permit: &PermitGuard<'_>,
602 ) -> Result<Self, crate::error::CompileError> {
603 permit.ensure_covers(handle_construction_peak_bytes(&vocab))?;
604 let canonical = Arc::new(CanonicalVocabulary::from_vocabulary_with_logits_vocab_size(
605 &vocab,
606 logits_vocab_size,
607 )?);
608 let fingerprint = canonical.fingerprint();
609 Ok(Self {
610 canonical,
611 fingerprint,
612 token_slices: Arc::new(OnceLock::new()),
613 })
614 }
615
616 pub fn from_packed(
619 token_bytes: &[u8],
620 byte_offsets: &[u32],
621 token_ids: &[u32],
622 id_offsets: &[u32],
623 eos_token_id: u32,
624 ) -> Result<Self, crate::error::CompileError> {
625 Self::from_packed_with_logits_vocab_size(
626 token_bytes,
627 byte_offsets,
628 token_ids,
629 id_offsets,
630 eos_token_id,
631 None,
632 )
633 }
634
635 #[allow(clippy::too_many_arguments)]
637 pub fn from_packed_with_logits_vocab_size(
638 token_bytes: &[u8],
639 byte_offsets: &[u32],
640 token_ids: &[u32],
641 id_offsets: &[u32],
642 eos_token_id: u32,
643 logits_vocab_size: Option<usize>,
644 ) -> Result<Self, crate::error::CompileError> {
645 let token_count = byte_offsets.len().saturating_sub(1);
646 let peak = projected_peak_bytes(token_bytes.len(), token_ids.len(), token_count);
647 let permit = acquire_vocab_build_bytes(peak)?;
648 Self::from_packed_admitted_with_logits_vocab_size(
649 token_bytes,
650 byte_offsets,
651 token_ids,
652 id_offsets,
653 eos_token_id,
654 logits_vocab_size,
655 &permit,
656 )
657 }
658
659 pub fn from_packed_admitted(
662 token_bytes: &[u8],
663 byte_offsets: &[u32],
664 token_ids: &[u32],
665 id_offsets: &[u32],
666 eos_token_id: u32,
667 permit: &PermitGuard<'_>,
668 ) -> Result<Self, crate::error::CompileError> {
669 Self::from_packed_admitted_with_logits_vocab_size(
670 token_bytes,
671 byte_offsets,
672 token_ids,
673 id_offsets,
674 eos_token_id,
675 None,
676 permit,
677 )
678 }
679
680 #[allow(clippy::too_many_arguments)]
683 pub fn from_packed_admitted_with_logits_vocab_size(
684 token_bytes: &[u8],
685 byte_offsets: &[u32],
686 token_ids: &[u32],
687 id_offsets: &[u32],
688 eos_token_id: u32,
689 logits_vocab_size: Option<usize>,
690 permit: &PermitGuard<'_>,
691 ) -> Result<Self, crate::error::CompileError> {
692 let token_count = byte_offsets.len().saturating_sub(1);
693 let required = projected_peak_bytes(token_bytes.len(), token_ids.len(), token_count);
694 permit.ensure_covers(required)?;
695 let canonical = Arc::new(CanonicalVocabulary::from_packed_with_logits_vocab_size(
696 token_bytes,
697 byte_offsets,
698 token_ids,
699 id_offsets,
700 eos_token_id,
701 logits_vocab_size,
702 )?);
703 let fingerprint = canonical.fingerprint();
704 Ok(Self {
705 canonical,
706 fingerprint,
707 token_slices: Arc::new(OnceLock::new()),
708 })
709 }
710
711 pub fn from_dense_id_ordered(
714 token_bytes: &[u8],
715 byte_offsets: &[u32],
716 present: &[bool],
717 eos_token_id: u32,
718 ) -> Result<Self, crate::error::CompileError> {
719 Self::from_dense_id_ordered_with_logits_vocab_size(
720 token_bytes,
721 byte_offsets,
722 present,
723 eos_token_id,
724 None,
725 )
726 }
727
728 pub fn from_dense_id_ordered_with_logits_vocab_size(
731 token_bytes: &[u8],
732 byte_offsets: &[u32],
733 present: &[bool],
734 eos_token_id: u32,
735 logits_vocab_size: Option<usize>,
736 ) -> Result<Self, crate::error::CompileError> {
737 let present_count = present.iter().filter(|&&p| p).count();
738 let peak = projected_peak_bytes(token_bytes.len(), present_count, present_count);
739 let permit = acquire_vocab_build_bytes(peak)?;
740 Self::from_dense_id_ordered_admitted_with_logits_vocab_size(
741 token_bytes,
742 byte_offsets,
743 present,
744 eos_token_id,
745 logits_vocab_size,
746 &permit,
747 )
748 }
749
750 pub fn from_dense_id_ordered_admitted(
753 token_bytes: &[u8],
754 byte_offsets: &[u32],
755 present: &[bool],
756 eos_token_id: u32,
757 permit: &PermitGuard<'_>,
758 ) -> Result<Self, crate::error::CompileError> {
759 Self::from_dense_id_ordered_admitted_with_logits_vocab_size(
760 token_bytes,
761 byte_offsets,
762 present,
763 eos_token_id,
764 None,
765 permit,
766 )
767 }
768
769 pub fn from_dense_id_ordered_admitted_with_logits_vocab_size(
772 token_bytes: &[u8],
773 byte_offsets: &[u32],
774 present: &[bool],
775 eos_token_id: u32,
776 logits_vocab_size: Option<usize>,
777 permit: &PermitGuard<'_>,
778 ) -> Result<Self, crate::error::CompileError> {
779 let present_count = present.iter().filter(|&&p| p).count();
780 let required = projected_peak_bytes(token_bytes.len(), present_count, present_count);
781 permit.ensure_covers(required)?;
782 let canonical = Arc::new(
783 CanonicalVocabulary::from_dense_id_ordered_with_logits_vocab_size(
784 token_bytes,
785 byte_offsets,
786 present,
787 eos_token_id,
788 logits_vocab_size,
789 )?,
790 );
791 let fingerprint = canonical.fingerprint();
792 Ok(Self {
793 canonical,
794 fingerprint,
795 token_slices: Arc::new(OnceLock::new()),
796 })
797 }
798
799 #[must_use]
801 pub(crate) fn canonical(&self) -> &Arc<CanonicalVocabulary> {
802 &self.canonical
803 }
804
805 #[must_use]
807 pub fn fingerprint(&self) -> VocabFingerprint {
808 self.fingerprint
809 }
810
811 #[must_use]
814 pub fn mask_vocab_size(&self) -> usize {
815 self.canonical.mask_vocab_size()
816 }
817
818 #[must_use]
820 pub fn eos_token_id(&self) -> u32 {
821 self.canonical.eos_token_id()
822 }
823
824 #[must_use]
827 pub fn len(&self) -> usize {
828 self.canonical.len()
829 }
830
831 #[must_use]
833 pub fn is_empty(&self) -> bool {
834 self.canonical.is_empty()
835 }
836
837 #[must_use]
839 pub fn token_bytes(&self, id: u32) -> Option<&[u8]> {
840 self.canonical.token_bytes(id)
841 }
842
843 pub fn iter_records(&self) -> impl Iterator<Item = (&[u8], &[u32])> {
845 self.canonical.iter()
846 }
847
848 pub(crate) fn prepared(&self) -> &PreparedVocabulary {
851 self.canonical.prepared()
852 }
853
854 pub(crate) fn token_slices(&self) -> Option<&Arc<TokenSliceCatalog>> {
855 self.token_slices
856 .get_or_init(|| TokenSliceCatalog::try_build(&self.canonical).ok().flatten())
857 .as_ref()
858 }
859
860 #[must_use]
862 pub fn token_slice_retained_bytes(&self) -> usize {
863 self.token_slices()
864 .map_or(0, |catalog| catalog.retained_bytes())
865 }
866}
867
868#[derive(Clone, Debug)]
871pub struct BoundByteTrie {
872 trie: Arc<VocabTrie>,
873 fingerprint: VocabFingerprint,
874}
875
876impl BoundByteTrie {
877 pub(crate) fn trie(&self) -> &VocabTrie {
878 &self.trie
879 }
880
881 pub(crate) fn trie_arc(&self) -> Arc<VocabTrie> {
883 self.trie.clone()
884 }
885
886 pub(crate) fn fingerprint(&self) -> VocabFingerprint {
887 self.fingerprint
888 }
889}
890
891#[cfg(test)]
892mod tests {
893 use super::*;
894 use crate::error::ErrorCode;
895 use crate::vocab::build_vocabulary;
896 use rustc_hash::FxHashMap as Map;
897
898 fn vocab(pairs: &[(&[u8], u32)], eos: u32) -> Vocabulary {
899 let mut map: Map<Vec<u8>, Vec<u32>> = Map::default();
900 for &(bytes, id) in pairs {
901 map.insert(bytes.to_vec(), vec![id]);
902 }
903 build_vocabulary(eos, map).expect("vocab")
904 }
905
906 #[test]
907 fn fingerprint_is_injective_over_marker_byte_id_lists() {
908 let fp = |pairs: &[(&[u8], &[u32])], eos: u32| {
909 let mut m: Map<Vec<u8>, Vec<u32>> = Map::default();
910 for &(b, ids) in pairs {
911 m.insert(b.to_vec(), ids.to_vec());
912 }
913 VocabFingerprint::of(&build_vocabulary(eos, m).expect("vocab")).expect("fingerprint")
914 };
915 let one_token = fp(&[(b"a", &[0x0000_00fe, 0x0000_00ff])], 9);
916 let two_tokens = fp(&[(b"a", &[0x0000_00fe]), (b"b", &[0x0000_00ff])], 9);
917 assert_ne!(
918 one_token, two_tokens,
919 "id-list grouping must change the fingerprint"
920 );
921 let reordered = fp(&[(b"a", &[0x0000_00ff, 0x0000_00fe])], 9);
922 assert_eq!(
923 one_token, reordered,
924 "id order within a token must not matter"
925 );
926 assert_ne!(
927 one_token,
928 fp(&[(b"a", &[0x0000_00fe, 0x0000_00ff])], 10),
929 "eos must change the fingerprint"
930 );
931 }
932
933 #[test]
934 fn same_vocabulary_returns_the_same_arc() {
935 let cache = TrieCache::new();
936 let v = vocab(&[(b"a", 0), (b"b", 1)], 9);
937 let first = cache.get_or_build_byte(&v).unwrap();
938 let second = cache.get_or_build_byte(&v).unwrap();
939 assert!(
940 Arc::ptr_eq(&first, &second),
941 "a cache hit must reuse the Arc"
942 );
943 }
944
945 #[test]
946 fn prevalidated_lookup_agrees_with_the_generic_one() {
947 let cache = TrieCache::new();
948 let v = vocab(&[(b"a", 0), (b"b", 1)], 9);
949 let generic = cache.get_or_build_byte(&v).unwrap();
950 let fp = VocabFingerprint::of(&v).unwrap();
951 let fast = cache.get_or_build_byte_prevalidated(&v, fp).unwrap();
952 assert!(
953 Arc::ptr_eq(&generic, &fast),
954 "same vocabulary, same cached Arc"
955 );
956 }
957
958 #[test]
959 fn a_handles_fingerprint_matches_the_direct_unprepared_computation() {
960 let cases = [
961 vocab(&[(b"a", 0), (b"b", 1)], 9),
962 vocab(&[(b"cat", 17), (b"car", 42)], 1000),
963 vocab(&[(&[0xff, 0xfe], 3), (&[0x00], 4)], 9999),
964 ];
965 for v in cases {
966 let direct = VocabFingerprint::of(&v).unwrap();
967 let via_handle = VocabularyHandle::new(Arc::new(v)).unwrap().fingerprint();
968 assert_eq!(
969 direct, via_handle,
970 "handle fingerprint must match the direct one"
971 );
972 }
973 }
974
975 #[test]
976 fn dense_id_ordered_handle_matches_the_tuple_handle_on_equivalent_content() {
977 let dense_v = vocab(&[(b"cat", 0), (b"car", 1), (b"c", 3)], 9999);
978 let via_new = VocabularyHandle::new(Arc::new(dense_v)).unwrap();
979
980 let token_bytes = b"catcarc";
981 let byte_offsets = [0u32, 3, 6, 6, 7]; let present = [true, true, false, true];
983 let via_dense =
984 VocabularyHandle::from_dense_id_ordered(token_bytes, &byte_offsets, &present, 9999)
985 .unwrap();
986
987 assert_eq!(via_new.fingerprint(), via_dense.fingerprint());
988 assert_eq!(via_new.mask_vocab_size(), via_dense.mask_vocab_size());
989 }
990
991 #[test]
992 fn dense_id_ordered_handle_rejects_eos_present_as_an_ordinary_slot() {
993 let token_bytes = b"ab";
994 let byte_offsets = [0u32, 1, 2];
995 let present = [true, true];
996 let err = VocabularyHandle::from_dense_id_ordered(token_bytes, &byte_offsets, &present, 1)
997 .expect_err("eos id 1 collides with the present 'b' slot");
998 assert_eq!(err.code, ErrorCode::MalformedTokenizer);
999 }
1000
1001 #[test]
1002 fn a_bind_hit_does_not_rebuild_and_only_the_miss_counts() {
1003 let cache = TrieCache::new();
1004 let handle = VocabularyHandle::new(Arc::new(vocab(&[(b"a", 0), (b"b", 1)], 9))).unwrap();
1005 let (h0, m0, _, _) = cache.stats();
1006 let _first = cache.bind(&handle).unwrap(); let (_h1, m1, _, _) = cache.stats();
1008 let _second = cache.bind(&handle).unwrap(); let (h2, m2, _, _) = cache.stats();
1010 assert_eq!(m1, m0 + 1, "the first bind is the only miss");
1011 assert_eq!(m2, m1, "a hit must not add a miss");
1012 assert_eq!(h2, h0 + 1, "the second bind is a hit");
1013 assert_eq!(cache.len(), 1, "one trie retained for one vocabulary");
1014 }
1015
1016 #[test]
1017 fn different_vocabularies_are_different_keys() {
1018 let cache = TrieCache::new();
1019 let a = vocab(&[(b"a", 0)], 9);
1020 let b = vocab(&[(b"b", 0)], 9);
1021 let ta = cache.get_or_build_byte(&a).unwrap();
1022 let tb = cache.get_or_build_byte(&b).unwrap();
1023 assert!(
1024 !Arc::ptr_eq(&ta, &tb),
1025 "distinct token maps must not share a trie"
1026 );
1027 assert_ne!(
1028 VocabFingerprint::of(&a).unwrap(),
1029 VocabFingerprint::of(&b).unwrap()
1030 );
1031 }
1032
1033 #[test]
1034 fn fingerprint_ignores_hashmap_order_but_tracks_eos() {
1035 let a = vocab(&[(b"a", 0), (b"b", 1)], 9);
1036 let b = vocab(&[(b"b", 1), (b"a", 0)], 9);
1037 assert_eq!(
1038 VocabFingerprint::of(&a).unwrap(),
1039 VocabFingerprint::of(&b).unwrap()
1040 );
1041 let eos_changed = vocab(&[(b"a", 0), (b"b", 1)], 8);
1042 assert_ne!(
1043 VocabFingerprint::of(&a).unwrap(),
1044 VocabFingerprint::of(&eos_changed).unwrap()
1045 );
1046 }
1047
1048 #[test]
1049 fn boundary_ambiguity_is_separated_by_domain_markers() {
1050 let one = vocab(&[(b"ab", 0)], 9);
1051 let two = vocab(&[(b"a", 0), (b"b", 1)], 9);
1052 assert_ne!(
1053 VocabFingerprint::of(&one).unwrap(),
1054 VocabFingerprint::of(&two).unwrap()
1055 );
1056 }
1057
1058 #[test]
1059 fn a_zero_budget_builds_but_retains_nothing() {
1060 let cache = TrieCache::with_budget(0);
1061 let v = vocab(&[(b"a", 0)], 9);
1062 let built = cache.get_or_build_byte(&v).expect("still built");
1063 assert_eq!(built.node_count(), 2);
1064 assert!(cache.is_empty(), "a zero budget retains no trie");
1065 }
1066
1067 #[test]
1068 fn a_budget_at_one_trie_retains_one_not_two() {
1069 let a = vocab(&[(b"a", 0)], 9);
1070 let budget = VocabTrie::build_byte(&a).unwrap().heap_bytes() + CACHE_ENTRY_OVERHEAD;
1071 let cache = TrieCache::with_budget(budget);
1072 cache.get_or_build_byte(&a).unwrap(); cache.get_or_build_byte(&vocab(&[(b"bb", 1)], 9)).unwrap(); assert_eq!(cache.len(), 1, "the budget retains exactly one trie");
1075 }
1076
1077 #[test]
1078 fn entry_overhead_is_charged_so_many_tiny_tries_cannot_exceed_the_budget() {
1079 let payload = VocabTrie::build_byte(&vocab(&[(b"a", 0)], 1000))
1080 .unwrap()
1081 .heap_bytes();
1082 let cache = TrieCache::with_budget(payload * 10);
1083 for i in 0..10u32 {
1084 cache
1085 .get_or_build_byte(&vocab(&[(&[b'a' + i as u8], i)], 1000))
1086 .unwrap();
1087 }
1088 assert!(
1089 cache.len() < 10,
1090 "charging per-entry overhead must retain fewer than a payload-only budget would ({})",
1091 cache.len()
1092 );
1093 assert!(
1094 cache.bytes() <= payload * 10,
1095 "retained bytes stay within budget"
1096 );
1097 }
1098
1099 #[test]
1100 fn single_flight_builds_a_cold_trie_exactly_once_under_concurrency() {
1101 use std::thread;
1102 let cache = Arc::new(TrieCache::new());
1103 let v = Arc::new(vocab(
1104 &[(b"a", 0), (b"bb", 1), (b"ccc", 2), (b"dddd", 3)],
1105 1000,
1106 ));
1107 let ptrs: Vec<usize> = (0..32)
1108 .map(|_| {
1109 let c = cache.clone();
1110 let v = v.clone();
1111 thread::spawn(move || Arc::as_ptr(&c.get_or_build_byte(&v).unwrap()) as usize)
1112 })
1113 .collect::<Vec<_>>()
1114 .into_iter()
1115 .map(|h| h.join().unwrap())
1116 .collect();
1117 assert!(
1118 ptrs.iter().all(|&p| p == ptrs[0]),
1119 "all callers share one trie"
1120 );
1121 let (hits, misses, _, _) = cache.stats();
1122 assert_eq!(misses, 1, "single-flight builds the cold trie exactly once");
1123 assert_eq!(hits, 31, "the other 31 callers hit the one build");
1124 assert_eq!(cache.len(), 1);
1125 }
1126
1127 #[test]
1128 fn single_flight_shares_one_build_even_when_the_budget_retains_nothing() {
1129 use std::sync::Barrier;
1130 use std::thread;
1131 let _serialize = DELAY_TEST_LOCK.lock().unwrap_or_else(|p| p.into_inner());
1132 let cache = Arc::new(TrieCache::with_budget(0));
1133 let v = Arc::new(vocab(
1134 &[(b"a", 0), (b"bb", 1), (b"ccc", 2), (b"dddd", 3)],
1135 1000,
1136 ));
1137 let barrier = Arc::new(Barrier::new(32));
1138 struct ResetDelay;
1139 impl Drop for ResetDelay {
1140 fn drop(&mut self) {
1141 DELAY_BUILDS.store(false, Ordering::Relaxed);
1142 }
1143 }
1144 DELAY_BUILDS.store(true, Ordering::Relaxed);
1145 let _reset = ResetDelay;
1146 let ptrs: Vec<usize> = (0..32)
1147 .map(|_| {
1148 let c = cache.clone();
1149 let v = v.clone();
1150 let b = barrier.clone();
1151 thread::spawn(move || {
1152 b.wait();
1153 Arc::as_ptr(&c.get_or_build_byte(&v).unwrap()) as usize
1154 })
1155 })
1156 .collect::<Vec<_>>()
1157 .into_iter()
1158 .map(|h| h.join().unwrap())
1159 .collect();
1160 assert!(
1161 ptrs.iter().all(|&p| p == ptrs[0]),
1162 "all callers share the one build"
1163 );
1164 let (_h, misses, _e, _o) = cache.stats();
1165 assert_eq!(misses, 1, "exactly one build even with nothing retained");
1166 assert!(cache.is_empty(), "a zero budget retains no trie");
1167 }
1168
1169 #[test]
1170 fn concurrent_distinct_builds_are_bounded_by_the_gate() {
1171 use std::thread;
1172 let cache = Arc::new(TrieCache::with_build_limit(1 << 28, 3));
1173 let handles: Vec<_> = (0..24u32)
1174 .map(|i| {
1175 let c = cache.clone();
1176 thread::spawn(move || {
1177 c.get_or_build_byte(&vocab(
1178 &[(&[b'a', i as u8], 0), (&[b'b', i as u8, 0], 1)],
1179 1000,
1180 ))
1181 .unwrap();
1182 })
1183 })
1184 .collect();
1185 for h in handles {
1186 h.join().unwrap();
1187 }
1188 assert!(
1189 cache.peak_concurrent_builds() <= 3,
1190 "simultaneous distinct builds must stay within the gate ({})",
1191 cache.peak_concurrent_builds()
1192 );
1193 assert_eq!(cache.len(), 24, "all 24 distinct tries built and retained");
1194 }
1195
1196 #[test]
1197 fn concurrent_builds_are_bounded_by_the_transient_byte_budget() {
1198 use std::sync::Barrier;
1199 use std::thread;
1200 let _serialize = DELAY_TEST_LOCK.lock().unwrap_or_else(|p| p.into_inner());
1201 let sample = vocab(&[(b"xa", 0), (b"xb", 1), (b"xc", 2)], 1000);
1202 let est = estimate_transient_build_bytes(&sample);
1203 let byte_budget = est * 2;
1204 let cache = Arc::new(TrieCache::with_build_gate(1 << 28, 8, byte_budget));
1205 let barrier = Arc::new(Barrier::new(8));
1206 DELAY_BUILDS.store(true, Ordering::Relaxed);
1207 struct ResetDelay;
1208 impl Drop for ResetDelay {
1209 fn drop(&mut self) {
1210 DELAY_BUILDS.store(false, Ordering::Relaxed);
1211 }
1212 }
1213 let _reset = ResetDelay;
1214 let handles: Vec<_> = (0..8u32)
1215 .map(|i| {
1216 let c = cache.clone();
1217 let b = barrier.clone();
1218 thread::spawn(move || {
1219 b.wait();
1220 c.get_or_build_byte(&vocab(
1221 &[
1222 (&[b'x', i as u8], 0),
1223 (&[b'y', i as u8], 1),
1224 (&[b'z', i as u8], 2),
1225 ],
1226 1000,
1227 ))
1228 .unwrap();
1229 })
1230 })
1231 .collect();
1232 for h in handles {
1233 h.join().unwrap();
1234 }
1235 assert_eq!(
1236 cache.peak_concurrent_builds(),
1237 2,
1238 "the byte budget, not the count cap of 8, must be the active constraint"
1239 );
1240 assert!(cache.peak_concurrent_build_bytes() <= byte_budget);
1241 assert_eq!(
1242 cache.len(),
1243 8,
1244 "all 8 distinct tries eventually built and retained"
1245 );
1246 }
1247
1248 #[test]
1249 fn an_oversized_build_estimate_is_rejected_and_shared_with_every_waiter() {
1250 use std::thread;
1251 let big = vocab(
1252 &[
1253 (b"big-token-one", 0),
1254 (b"big-token-two", 1),
1255 (b"big-token-three", 2),
1256 ],
1257 1000,
1258 );
1259 let est = estimate_transient_build_bytes(&big);
1260 let cache = Arc::new(TrieCache::with_build_gate(1 << 28, 8, est - 1));
1261 let handles: Vec<_> = (0..8)
1262 .map(|_| {
1263 let c = cache.clone();
1264 let v = big.clone();
1265 thread::spawn(move || c.get_or_build_byte(&v).map_err(|e| e.code))
1266 })
1267 .collect();
1268 for h in handles {
1269 let result = h.join().unwrap();
1270 assert_eq!(
1271 result.unwrap_err(),
1272 ErrorCode::InternalLimitExceeded,
1273 "an over-budget build must be rejected, not silently clamped and run"
1274 );
1275 }
1276 assert!(cache.is_empty(), "a rejected build is never cached");
1277 }
1278
1279 #[test]
1280 fn a_hot_vocabulary_becomes_and_stays_cached_after_a_junk_burst_fills_the_cache() {
1281 let sample = vocab(&[(&[b'j', 0, 0], 0)], 1000);
1282 let entry_bytes =
1283 VocabTrie::build_byte(&sample).unwrap().heap_bytes() + CACHE_ENTRY_OVERHEAD;
1284 let cache = TrieCache::with_budget(entry_bytes * 3);
1285 for i in 0..50u32 {
1286 let junk = vocab(&[(&[b'j', (i % 251) as u8, (i / 251) as u8], i)], 1000);
1287 cache.get_or_build_byte(&junk).unwrap();
1288 }
1289 let (_, _, evictions_from_junk, _) = cache.stats();
1290 assert!(
1291 evictions_from_junk > 0,
1292 "the junk burst overflowed the budget"
1293 );
1294
1295 let hot = vocab(&[(b"hot-token-aaaa", 0), (b"hot-token-bbbb", 1)], 1000);
1296 let (_, misses0, _, _) = cache.stats();
1297 cache.get_or_build_byte(&hot).unwrap();
1298 let (_, misses1, _, _) = cache.stats();
1299 assert_eq!(
1300 misses1,
1301 misses0 + 1,
1302 "the first access to a new hot vocab is a miss"
1303 );
1304
1305 cache.get_or_build_byte(&hot).unwrap();
1306 let (_, misses2, _, _) = cache.stats();
1307 assert_eq!(misses2, misses1, "the second access hits the cached entry");
1308 }
1309
1310 #[test]
1311 fn old_bypass_only_policy_would_fail_the_hot_vocabulary_regression() {
1312 let cache = TrieCache::with_budget(0);
1313 let hot = vocab(&[(b"hot-token-aaaa", 0), (b"hot-token-bbbb", 1)], 1000);
1314 cache.get_or_build_byte(&hot).unwrap();
1315 let (_, misses1, _, _) = cache.stats();
1316 cache.get_or_build_byte(&hot).unwrap();
1317 let (_, misses2, _, _) = cache.stats();
1318 assert_eq!(
1319 misses2,
1320 misses1 + 1,
1321 "a bypass-only cache never turns a repeat into a hit"
1322 );
1323 }
1324
1325 #[test]
1326 fn concurrent_native_threads_share_one_trie_and_never_corrupt_the_cache() {
1327 use std::thread;
1328 let cache = Arc::new(TrieCache::new());
1329 let same = Arc::new(vocab(&[(b"a", 0), (b"bb", 1), (b"ccc", 2)], 1000));
1330 let mut handles = Vec::new();
1331 for _ in 0..16 {
1332 let c = cache.clone();
1333 let v = same.clone();
1334 handles.push(thread::spawn(move || {
1335 c.get_or_build_byte(&v).map(|t| Arc::as_ptr(&t) as usize)
1336 }));
1337 }
1338 for i in 0..16u32 {
1339 let c = cache.clone();
1340 handles.push(thread::spawn(move || {
1341 c.get_or_build_byte(&vocab(&[(&[b'z', i as u8], i)], 1000))
1342 .map(|t| Arc::as_ptr(&t) as usize)
1343 }));
1344 }
1345 let ptrs: Vec<_> = handles
1346 .into_iter()
1347 .map(|h| h.join().unwrap().unwrap())
1348 .collect();
1349 let shared = Arc::as_ptr(&cache.get_or_build_byte(&same).unwrap()) as usize;
1350 for p in &ptrs[..16] {
1351 assert_eq!(*p, shared, "same vocab must resolve to one cached trie");
1352 }
1353 assert_eq!(
1354 cache.len(),
1355 17,
1356 "one shared trie plus 16 distinct tries retained"
1357 );
1358 }
1359
1360 #[test]
1361 fn clear_drops_retained_tries() {
1362 let cache = TrieCache::new();
1363 cache.get_or_build_byte(&vocab(&[(b"a", 0)], 9)).unwrap();
1364 cache.get_or_build_byte(&vocab(&[(b"b", 0)], 9)).unwrap();
1365 assert_eq!(cache.len(), 2);
1366 cache.clear();
1367 assert!(cache.is_empty());
1368 }
1369
1370 #[test]
1371 fn clear_also_drops_the_lru_log_so_repeated_fill_clear_does_not_leak() {
1372 let cache = TrieCache::new();
1373 for cycle in 0..50u32 {
1374 cache
1375 .get_or_build_byte(&vocab(&[(&[cycle as u8], 0)], 9))
1376 .unwrap();
1377 cache.clear();
1378 }
1379 let lru_len = cache
1380 .state
1381 .lock()
1382 .unwrap_or_else(|p| p.into_inner())
1383 .lru
1384 .len();
1385 assert_eq!(
1386 lru_len, 0,
1387 "clear() must drop the LRU log, not just the map"
1388 );
1389 }
1390
1391 #[test]
1392 fn stats_are_a_lifetime_total_not_reset_by_clear() {
1393 let cache = TrieCache::new();
1394 cache.get_or_build_byte(&vocab(&[(b"a", 0)], 9)).unwrap();
1395 cache.get_or_build_byte(&vocab(&[(b"a", 0)], 9)).unwrap(); let (hits, misses, ..) = cache.stats();
1397 assert_eq!((hits, misses), (1, 1));
1398 cache.clear();
1399 let (hits_after, misses_after, ..) = cache.stats();
1400 assert_eq!(
1401 (hits_after, misses_after),
1402 (1, 1),
1403 "clear() must not reset traffic counters"
1404 );
1405 }
1406
1407 #[test]
1408 fn an_evicted_trie_stays_valid_and_correct_while_a_caller_still_holds_its_arc() {
1409 let a = vocab(&[(b"a", 0), (b"ab", 1)], 9);
1410 let budget = VocabTrie::build_byte(&a).unwrap().heap_bytes() + CACHE_ENTRY_OVERHEAD;
1411 let cache = TrieCache::with_budget(budget);
1412
1413 let held = cache.get_or_build_byte(&a).unwrap();
1414 assert_eq!(cache.len(), 1);
1415
1416 cache.get_or_build_byte(&vocab(&[(b"z", 0)], 9)).unwrap();
1417 assert_eq!(cache.len(), 1, "the budget still retains exactly one entry");
1418
1419 assert_eq!(
1420 held.node_count(),
1421 3,
1422 "evicted trie's structure is untouched"
1423 );
1424
1425 let rebuilt = cache.get_or_build_byte(&a).unwrap();
1426 assert_eq!(
1427 rebuilt.node_count(),
1428 held.node_count(),
1429 "a rebuild after eviction reproduces the identical structure"
1430 );
1431 }
1432
1433 #[test]
1434 fn a_panicking_build_releases_its_permit_and_slot_so_the_next_build_is_not_stuck() {
1435 let cache = TrieCache::with_build_limit(usize::MAX, 1); let v = vocab(&[(b"a", 0)], 9);
1437
1438 PANIC_ON_BUILD.with(|p| p.set(true));
1439 let result =
1440 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| cache.get_or_build_byte(&v)));
1441 PANIC_ON_BUILD.with(|p| p.set(false));
1442 assert!(result.is_err(), "the injected panic must actually unwind");
1443
1444 let recovered = cache
1445 .get_or_build_byte(&v)
1446 .expect("a fresh build must succeed");
1447 assert_eq!(recovered.node_count(), 2);
1448 }
1449
1450 #[test]
1451 fn prepared_transient_estimate_charges_many_ids_under_one_record_like_the_vocab_estimate() {
1452 let mut map: Map<Vec<u8>, Vec<u32>> = Map::default();
1453 map.insert(b"a".to_vec(), (0..5000).collect());
1454 let v = build_vocabulary(6000, map).expect("vocab");
1455 let prepared = PreparedVocabulary::build(&v).expect("prepared");
1456 assert_eq!(
1457 estimate_transient_build_bytes_prepared(&prepared),
1458 estimate_transient_build_bytes(&v)
1459 );
1460 }
1461}