1use anyhow::{Context, Result};
2use byteorder::{ByteOrder, LittleEndian};
3use rkv::backend::{Lmdb, LmdbDatabase, LmdbEnvironment, LmdbRwTransaction};
4use rkv::{MultiStore, Rkv, SingleStore, StoreOptions, Value, Writer};
5use std::fs;
6use std::path::{Path, PathBuf};
7use std::sync::Mutex;
8use std::sync::OnceLock;
9
10type SStore = SingleStore<LmdbDatabase>;
11type MStore = MultiStore<LmdbDatabase>;
12type IndexType = u32;
13
14const INDEX_DIR_NAME: &str = "cu29_log_index";
16
17fn parent_n_times(path: &Path, n: usize) -> Option<PathBuf> {
18 let mut result = Some(path.to_path_buf());
19 for _ in 0..n {
20 result = result?.parent().map(PathBuf::from);
21 }
22 result
23}
24
25pub fn default_log_index_dir() -> PathBuf {
27 let outdir = std::env::var("LOG_INDEX_DIR").expect("no LOG_INDEX_DIR system variable set, be sure build.rs sets it, see cu29_log/build.rs for example.");
28 let outdir_path = Path::new(&outdir);
29 parent_n_times(outdir_path, 3).unwrap().join(INDEX_DIR_NAME)
30}
31
32pub fn read_interned_strings(index: &Path) -> Result<Vec<String>> {
35 let mut all_strings = Vec::<String>::new();
36 let env =
37 Rkv::new::<Lmdb>(index).context("Could not open the string index. Check the path.")?;
38
39 let index_to_string = env
40 .open_single("index_to_string", StoreOptions::default())
41 .context("Could not open the index_to_string store")?;
42 let db_reader = env.read().unwrap();
43 let ri = index_to_string.iter_start(&db_reader);
44 let mut i = ri.expect("Failed to start iterator");
45 while let Some(Ok(v)) = i.next() {
46 let (k, v) = v;
47 let index = LittleEndian::read_u32(k) as usize;
48
49 if let rkv::Value::Str(s) = v {
50 if all_strings.len() <= index {
51 all_strings.resize(index + 1, String::new());
52 }
53
54 all_strings[index] = s.to_string();
55 }
56 }
57 Ok(all_strings)
58}
59
60#[cfg(feature = "macro_debug")]
61const COLORED_PREFIX_BUILD_LOG: &str = "\x1b[32mCLog:\x1b[0m";
62
63#[cfg(feature = "macro_debug")]
64macro_rules! build_log {
65 ($($arg:tt)*) => {
66 eprintln!("{} {}", COLORED_PREFIX_BUILD_LOG, format!($($arg)*));
67 };
68}
69
70static RKV: OnceLock<Mutex<Rkv<LmdbEnvironment>>> = OnceLock::new();
71static DBS: OnceLock<Mutex<(SStore, SStore, SStore, MStore)>> = OnceLock::new();
72
73fn rkv() -> &'static Mutex<Rkv<LmdbEnvironment>> {
74 RKV.get_or_init(|| {
75 let target_dir = default_log_index_dir();
76
77 if !target_dir.exists() {
79 fs::create_dir_all(&target_dir).unwrap();
80 }
81
82 #[cfg(feature = "macro_debug")]
83 {
84 build_log!(
85 "=================================================================================="
86 );
87 build_log!("Interned strings are stored in: {:?}", target_dir);
88 build_log!(" [r] is reused index and [n] is new index.");
89 build_log!(
90 "=================================================================================="
91 );
92 }
93
94 let env = Rkv::new::<Lmdb>(&target_dir).unwrap();
95 Mutex::new(env)
96 })
97}
98
99fn dbs() -> &'static Mutex<(SStore, SStore, SStore, MStore)> {
100 DBS.get_or_init(|| {
101 let env = rkv().lock().unwrap();
102
103 let counter = env.open_single("counter", StoreOptions::create()).unwrap();
104 let index_to_string = env
105 .open_single("index_to_string", StoreOptions::create())
106 .unwrap();
107 let string_to_index = env
108 .open_single("string_to_index", StoreOptions::create())
109 .unwrap();
110 let index_to_callsites = env
111 .open_multi("index_to_callsites", StoreOptions::create())
112 .unwrap();
113
114 Mutex::new((
115 counter,
116 index_to_string,
117 string_to_index,
118 index_to_callsites,
119 ))
120 })
121}
122
123pub fn intern_string(s: &str) -> Option<IndexType> {
124 let (counter_store, index_to_string, string_to_index, _) = &mut *dbs().lock().unwrap();
125 let index = {
126 let env = rkv().lock().unwrap();
127 {
129 let reader = env.read().unwrap();
130 if let Ok(Some(Value::U64(index))) = string_to_index.get(&reader, s) {
132 #[cfg(feature = "macro_debug")]
133 {
134 build_log!("#{:0>3} [r] -> {}.", index, s);
135 }
136 return Some(index as IndexType);
137 };
138 }
139 let mut writer = env.write().unwrap();
140 let next_index = get_next_index(&mut writer, counter_store).unwrap();
141 index_to_string
143 .put(&mut writer, next_index.to_le_bytes(), &Value::Str(s))
144 .unwrap();
145 string_to_index
146 .put(&mut writer, s, &Value::U64(next_index as u64))
147 .unwrap();
148 writer.commit().unwrap();
149 Some(next_index)
150 };
151 #[cfg(feature = "macro_debug")]
152 {
153 build_log!("#{:0>3} [n] -> {}.", index.unwrap(), s);
154 }
155 index
156}
157
158#[allow(dead_code)]
159pub fn record_callsite(filename: &str, line_number: u32) -> Option<IndexType> {
160 intern_string(format!("{filename}:{line_number}").as_str())
161}
162
163const COUNTER_KEY: &str = "__counter__";
164fn get_next_index(
165 writer: &mut Writer<LmdbRwTransaction>,
166 counter_store: &SStore,
167) -> Result<IndexType, Box<dyn std::error::Error>> {
168 let current_counter = match counter_store.get(writer, COUNTER_KEY)? {
169 Some(Value::U64(value)) => value as IndexType,
170 _ => 0,
171 };
172
173 let next_counter = current_counter + 1;
174 counter_store.put(writer, COUNTER_KEY, &Value::U64(next_counter as u64))?;
175 Ok(next_counter)
176}