code_moniker_workspace/snapshot/inventory/
mod.rs1mod catalog;
2mod facets;
3mod set;
4
5use std::collections::BTreeSet;
6use std::hash::{Hash, Hasher};
7use std::sync::Arc;
8
9use code_moniker_core::core::shape::Shape;
10use code_moniker_core::core::uri::{UriConfig, from_uri};
11use rustc_hash::FxHashMap;
12
13use super::{RecordTable, ResourceGeneration, SourceFileRecord, SourceId, SymbolId, SymbolRecord};
14
15pub use catalog::SymbolOrdinalCatalog;
16pub use facets::SymbolInventoryFacets;
17pub use set::SymbolSet;
18
19#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
20pub struct SymbolOrdinal(u32);
21
22impl SymbolOrdinal {
23 pub fn from_index(value: usize) -> Self {
24 assert!(
25 u32::try_from(value).is_ok(),
26 "symbol index exceeds u32 range"
27 );
28 Self(value as u32)
29 }
30
31 pub fn as_usize(self) -> usize {
32 self.0 as usize
33 }
34
35 pub fn raw(self) -> u32 {
36 self.0
37 }
38}
39
40#[derive(Clone, Debug, Eq, Hash, PartialEq)]
41pub struct InventorySegment {
42 pub kind: Arc<str>,
43 pub name: Arc<str>,
44}
45
46#[derive(Clone, Debug, Eq, PartialEq)]
47pub struct InventorySymbol {
48 pub id: SymbolId,
49 pub source: SourceId,
50 pub identity: Arc<str>,
51 pub name: Arc<str>,
52 pub kind: Arc<str>,
53 pub shape: Arc<str>,
54 pub visibility: Arc<str>,
55 pub language: Arc<str>,
56 pub source_path: Arc<str>,
57 pub source_root: usize,
58 pub srcset: Arc<str>,
59 pub line_range: Option<(u32, u32)>,
60 pub parent: Option<SymbolId>,
61 pub segments: Arc<[InventorySegment]>,
62}
63
64#[derive(Clone, Debug, Eq, PartialEq)]
65pub struct SymbolInventoryIndex {
66 generation: ResourceGeneration,
67 catalog: Arc<SymbolOrdinalCatalog>,
68 records: FxHashMap<SymbolOrdinal, InventorySymbol>,
69 all_symbols: SymbolSet,
70 facets: SymbolInventoryFacets,
71 compact_identities: FxHashMap<u64, SymbolOrdinal>,
72 compact_identity_collisions: FxHashMap<u64, Vec<SymbolOrdinal>>,
73}
74
75impl Default for SymbolInventoryIndex {
76 fn default() -> Self {
77 Self::empty(ResourceGeneration::new(0))
78 }
79}
80
81impl SymbolInventoryIndex {
82 pub(crate) fn estimated_heap_bytes(&self) -> usize {
83 let mut strings = std::collections::HashSet::<(usize, usize)>::new();
84 let mut string_bytes = 0usize;
85 let mut segment_bytes = 0usize;
86 for record in self.records.values() {
87 for value in [
88 &record.identity,
89 &record.name,
90 &record.kind,
91 &record.shape,
92 &record.visibility,
93 &record.language,
94 &record.source_path,
95 &record.srcset,
96 ] {
97 if strings.insert((value.as_ptr() as usize, value.len())) {
98 string_bytes += value.len();
99 }
100 }
101 segment_bytes += record.segments.len() * std::mem::size_of::<InventorySegment>();
102 for segment in record.segments.iter() {
103 for value in [&segment.kind, &segment.name] {
104 if strings.insert((value.as_ptr() as usize, value.len())) {
105 string_bytes += value.len();
106 }
107 }
108 }
109 }
110 self.records.capacity()
111 * (std::mem::size_of::<SymbolOrdinal>() + std::mem::size_of::<InventorySymbol>())
112 + self.catalog.estimated_heap_bytes()
113 + self.all_symbols.estimated_heap_bytes()
114 + self.facets.estimated_heap_bytes()
115 + self.compact_identities.capacity()
116 * (std::mem::size_of::<u64>() + std::mem::size_of::<SymbolOrdinal>())
117 + self
118 .compact_identity_collisions
119 .values()
120 .map(|ordinals| ordinals.capacity() * std::mem::size_of::<SymbolOrdinal>())
121 .sum::<usize>()
122 + self.compact_identity_collisions.capacity()
123 * (std::mem::size_of::<u64>() + std::mem::size_of::<Vec<SymbolOrdinal>>())
124 + segment_bytes
125 + string_bytes
126 }
127
128 pub fn empty(generation: ResourceGeneration) -> Self {
129 Self {
130 generation,
131 catalog: Arc::new(SymbolOrdinalCatalog::default()),
132 records: FxHashMap::default(),
133 all_symbols: SymbolSet::new(),
134 facets: SymbolInventoryFacets::default(),
135 compact_identities: FxHashMap::default(),
136 compact_identity_collisions: FxHashMap::default(),
137 }
138 }
139
140 pub fn build(
141 generation: ResourceGeneration,
142 sources: &[SourceFileRecord],
143 symbols: &RecordTable<SymbolRecord>,
144 ) -> Self {
145 let mut inventory = Self::empty(generation);
146 for symbol in symbols.iter() {
147 let fallback = missing_source(symbol.source);
148 let source = sources.get(symbol.source.file()).unwrap_or(&fallback);
149 index_record(&mut inventory, symbol, source, None);
150 }
151 inventory
152 }
153
154 pub fn refresh(
155 &self,
156 generation: ResourceGeneration,
157 sources: &[SourceFileRecord],
158 symbols: &RecordTable<SymbolRecord>,
159 changed_files: &BTreeSet<usize>,
160 ) -> Self {
161 refresh_inventory(self, generation, sources, symbols, changed_files)
162 }
163
164 pub fn generation(&self) -> ResourceGeneration {
165 self.generation
166 }
167
168 pub fn catalog(&self) -> &Arc<SymbolOrdinalCatalog> {
169 &self.catalog
170 }
171
172 pub fn all_symbols(&self) -> &SymbolSet {
173 &self.all_symbols
174 }
175
176 pub fn record(&self, ordinal: SymbolOrdinal) -> Option<&InventorySymbol> {
177 self.records.get(&ordinal)
178 }
179
180 pub fn record_by_id(&self, id: &SymbolId) -> Option<&InventorySymbol> {
181 self.catalog
182 .ordinal(id)
183 .and_then(|ordinal| self.record(ordinal))
184 }
185
186 pub fn symbol_ids_by_identity(&self, identity: &str) -> Vec<SymbolId> {
187 self.facets
188 .symbols_by_identity(identity)
189 .into_iter()
190 .flat_map(SymbolSet::iter)
191 .filter_map(|ordinal| self.catalog.id(ordinal).copied())
192 .collect()
193 }
194
195 pub fn symbol_ids_by_compact_identity(&self, compact: &str) -> Vec<SymbolId> {
196 let hash = compact_identity_hash(compact);
197 let candidates = self
198 .compact_identity_collisions
199 .get(&hash)
200 .map(Vec::as_slice)
201 .or_else(|| self.compact_identities.get(&hash).map(std::slice::from_ref))
202 .unwrap_or_default();
203 candidates
204 .iter()
205 .copied()
206 .filter_map(|ordinal| self.record(ordinal))
207 .filter(|record| {
208 compact_record_identity(record.identity.as_ref()).as_deref() == Some(compact)
209 })
210 .map(|record| record.id)
211 .collect()
212 }
213
214 pub fn facets(&self) -> &SymbolInventoryFacets {
215 &self.facets
216 }
217}
218
219fn index_record(
220 inventory: &mut SymbolInventoryIndex,
221 symbol: &SymbolRecord,
222 source: &SourceFileRecord,
223 preferred_ordinal: Option<SymbolOrdinal>,
224) {
225 let catalog = Arc::make_mut(&mut inventory.catalog);
226 let ordinal = match preferred_ordinal {
227 Some(ordinal) => {
228 catalog.bind_id(ordinal, symbol.id);
229 ordinal
230 }
231 None => catalog.push(symbol.id),
232 };
233 if inventory.records.contains_key(&ordinal) {
234 unindex_record(inventory, ordinal);
235 }
236 let record = inventory_record(symbol, source);
237 inventory.all_symbols.insert(ordinal);
238 facets::insert_facets(&mut inventory.facets, &record, ordinal);
239 if let Some(compact) = compact_record_identity(record.identity.as_ref()) {
240 index_compact_identity(inventory, compact_identity_hash(&compact), ordinal);
241 }
242 inventory.records.insert(ordinal, record);
243}
244
245fn inventory_record(symbol: &SymbolRecord, source: &SourceFileRecord) -> InventorySymbol {
246 let segments = parse_segments(&symbol.identity);
247 let srcset = segments
248 .iter()
249 .filter(|segment| segment.kind.as_ref() == "srcset")
250 .map(|segment| segment.name.as_ref())
251 .collect::<Vec<_>>()
252 .join(".");
253 InventorySymbol {
254 id: symbol.id,
255 source: symbol.source,
256 identity: Arc::clone(&symbol.identity),
257 name: Arc::from(symbol.name.as_str()),
258 kind: Arc::from(symbol.kind.as_str()),
259 shape: Arc::from(Shape::for_kind(symbol.kind.as_bytes()).as_str()),
260 visibility: Arc::from(symbol.visibility.as_str()),
261 language: Arc::from(source.language.as_str()),
262 source_path: Arc::from(source.path.as_str()),
263 source_root: source.source_root,
264 srcset: Arc::from(srcset),
265 line_range: symbol.line_range,
266 parent: symbol.parent,
267 segments: Arc::from(segments),
268 }
269}
270
271fn parse_segments(identity: &str) -> Vec<InventorySegment> {
272 let marker = "+moniker://";
273 let scheme_end = identity.find(marker).map(|index| index + marker.len());
274 let Some(scheme) = scheme_end.and_then(|end| identity.get(..end)) else {
275 return Vec::new();
276 };
277 let Ok(moniker) = from_uri(identity, &UriConfig { scheme }) else {
278 return Vec::new();
279 };
280 moniker
281 .as_view()
282 .segments()
283 .filter_map(|segment| {
284 Some(InventorySegment {
285 kind: Arc::from(std::str::from_utf8(segment.kind).ok()?),
286 name: Arc::from(std::str::from_utf8(segment.name).ok()?),
287 })
288 })
289 .collect()
290}
291
292fn unindex_record(inventory: &mut SymbolInventoryIndex, ordinal: SymbolOrdinal) {
293 let Some(record) = inventory.records.remove(&ordinal) else {
294 return;
295 };
296 inventory.all_symbols.remove(ordinal);
297 facets::remove_facets(&mut inventory.facets, &record, ordinal);
298 if let Some(compact) = compact_record_identity(record.identity.as_ref()) {
299 let hash = compact_identity_hash(&compact);
300 unindex_compact_identity(inventory, hash, ordinal);
301 }
302}
303
304fn index_compact_identity(inventory: &mut SymbolInventoryIndex, hash: u64, ordinal: SymbolOrdinal) {
305 let Some(existing) = inventory.compact_identities.get(&hash).copied() else {
306 inventory.compact_identities.insert(hash, ordinal);
307 return;
308 };
309 let collisions = inventory
310 .compact_identity_collisions
311 .entry(hash)
312 .or_insert_with(|| vec![existing]);
313 if !collisions.contains(&ordinal) {
314 collisions.push(ordinal);
315 }
316}
317
318fn unindex_compact_identity(
319 inventory: &mut SymbolInventoryIndex,
320 hash: u64,
321 ordinal: SymbolOrdinal,
322) {
323 if let Some(collisions) = inventory.compact_identity_collisions.get_mut(&hash) {
324 collisions.retain(|candidate| *candidate != ordinal);
325 match collisions.as_slice() {
326 [] => {
327 inventory.compact_identities.remove(&hash);
328 inventory.compact_identity_collisions.remove(&hash);
329 }
330 [remaining] => {
331 inventory.compact_identities.insert(hash, *remaining);
332 inventory.compact_identity_collisions.remove(&hash);
333 }
334 _ => {}
335 }
336 } else if inventory.compact_identities.get(&hash) == Some(&ordinal) {
337 inventory.compact_identities.remove(&hash);
338 }
339}
340
341fn compact_identity_hash(compact: &str) -> u64 {
342 let mut hasher = rustc_hash::FxHasher::default();
343 compact.hash(&mut hasher);
344 hasher.finish()
345}
346
347fn compact_record_identity(identity: &str) -> Option<String> {
348 let scheme_end = identity.find("://")? + 3;
349 let scheme = &identity[..scheme_end];
350 crate::code::compact_identity(identity, scheme)
351}
352
353fn refresh_inventory(
354 previous: &SymbolInventoryIndex,
355 generation: ResourceGeneration,
356 sources: &[SourceFileRecord],
357 symbols: &RecordTable<SymbolRecord>,
358 changed_files: &BTreeSet<usize>,
359) -> SymbolInventoryIndex {
360 let mut inventory = previous.clone();
361 inventory.generation = generation;
362 let pending_retire = changed_ordinals(&inventory, changed_files);
363 let mut reusable_ordinals = FxHashMap::<(SourceId, Arc<str>), Vec<SymbolOrdinal>>::default();
364 for ordinal in &pending_retire {
365 if let Some(record) = inventory.record(*ordinal) {
366 reusable_ordinals
367 .entry((record.source, Arc::clone(&record.identity)))
368 .or_default()
369 .push(*ordinal);
370 }
371 }
372 for ordinal in &pending_retire {
373 unindex_record(&mut inventory, *ordinal);
374 Arc::make_mut(&mut inventory.catalog).unbind_id(*ordinal);
375 }
376 for file in changed_files {
377 for symbol in symbols.file_records(*file) {
378 let fallback = missing_source(symbol.source);
379 let source = sources.get(*file).unwrap_or(&fallback);
380 let key = (symbol.source, Arc::clone(&symbol.identity));
381 let preferred = reusable_ordinals.get_mut(&key).and_then(Vec::pop);
382 index_record(&mut inventory, symbol, source, preferred);
383 }
384 }
385 for ordinal in pending_retire {
386 if inventory.catalog.id(ordinal).is_none() {
387 Arc::make_mut(&mut inventory.catalog).retire(ordinal);
388 }
389 }
390 inventory
391}
392
393fn missing_source(source: SourceId) -> SourceFileRecord {
394 SourceFileRecord {
395 id: source,
396 uri: String::new(),
397 source_root: 0,
398 path: String::new(),
399 rel_path: String::new(),
400 anchor: String::new(),
401 language: String::new(),
402 text: String::new(),
403 }
404}
405
406fn changed_ordinals(
407 inventory: &SymbolInventoryIndex,
408 changed_files: &BTreeSet<usize>,
409) -> Vec<SymbolOrdinal> {
410 let mut changed = SymbolSet::new();
411 for file in changed_files {
412 if let Some(symbols) = inventory.facets.symbols_by_source(SourceId::at(*file)) {
413 changed.union_with(symbols);
414 }
415 }
416 changed.iter().collect()
417}
418
419#[cfg(test)]
420mod tests;