Skip to main content

code_moniker_workspace/
memory.rs

1//! Canonical runtime estimates for the immutable workspace snapshot.
2//!
3//! These values are deliberately lower-bound estimates of owned heap storage.
4//! Allocator metadata and storage hidden inside third-party collections are not
5//! observable here; the contract is stable comparison, not exact accounting.
6
7use std::collections::HashSet;
8use std::mem::{size_of, size_of_val};
9use std::sync::Arc;
10
11use code_moniker_core::core::code_graph::{DefRecord, RefRecord};
12
13use crate::snapshot::{CodeIndex, LinkageSnapshot, WorkspaceSnapshot};
14use crate::source::{CodeIndexMaterial, IndexedSourceFile};
15
16#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
17pub struct SnapshotMemoryEstimate {
18	pub source_bytes: usize,
19	pub index_bytes: usize,
20	pub graph_bytes: usize,
21}
22
23#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
24pub struct RetainedMaterialMemoryEstimate {
25	pub source_bytes: usize,
26	pub graph_bytes: usize,
27	pub lookup_bytes: usize,
28	pub metadata_bytes: usize,
29	pub total_bytes: usize,
30}
31
32impl RetainedMaterialMemoryEstimate {
33	pub fn from_material(material: &CodeIndexMaterial) -> Self {
34		let source_bytes = material
35			.files
36			.iter()
37			.map(|file| file.source.capacity())
38			.sum();
39		let metadata_bytes = material.files.capacity() * size_of::<Arc<IndexedSourceFile>>()
40			+ material
41				.files
42				.iter()
43				.map(|file| {
44					size_of::<IndexedSourceFile>()
45						+ file.source_uri.capacity()
46						+ file.path.as_os_str().len()
47						+ file.rel_path.as_os_str().len()
48						+ file.anchor.as_os_str().len()
49				})
50				.sum::<usize>();
51		let graph_bytes =
52			material
53				.files
54				.iter()
55				.map(|file| {
56					file.graph.def_count() * size_of::<DefRecord>()
57						+ file.graph.ref_count() * size_of::<RefRecord>()
58						+ file
59							.graph
60							.defs()
61							.map(definition_payload_bytes)
62							.sum::<usize>() + file
63						.graph
64						.refs()
65						.map(reference_payload_bytes)
66						.sum::<usize>() + file
67						.graph
68						.defs()
69						.map(|definition| definition.moniker.as_encoded().len())
70						.sum::<usize>()
71				})
72				.sum();
73		let lookup_bytes = material
74			.symbols_by_moniker
75			.iter()
76			.map(|(moniker, symbol)| moniker.as_encoded().len() + size_of_val(symbol))
77			.sum();
78		Self {
79			source_bytes,
80			graph_bytes,
81			lookup_bytes,
82			metadata_bytes,
83			total_bytes: source_bytes + graph_bytes + lookup_bytes + metadata_bytes,
84		}
85	}
86}
87
88impl SnapshotMemoryEstimate {
89	pub fn from_snapshot(snapshot: &WorkspaceSnapshot) -> Self {
90		Self {
91			source_bytes: source_bytes(&snapshot.index),
92			index_bytes: Self::from_index(&snapshot.index),
93			graph_bytes: Self::from_linkage(&snapshot.linkage),
94		}
95	}
96
97	pub fn from_index(index: &CodeIndex) -> usize {
98		let sources = index.sources.iter().fold(
99			index.sources.capacity() * size_of::<crate::snapshot::SourceFileRecord>(),
100			|total, source| {
101				total
102					+ source.uri.capacity()
103					+ source.path.capacity()
104					+ source.rel_path.capacity()
105					+ source.anchor.capacity()
106					+ source.language.capacity()
107					+ source.text.capacity()
108			},
109		);
110		let symbols = index.symbols.estimated_heap_bytes()
111			+ index.symbols.iter().fold(0, |total, symbol| {
112				total
113					+ symbol.name.capacity()
114					+ symbol.kind.capacity()
115					+ symbol.visibility.capacity()
116					+ symbol.signature.capacity()
117					+ symbol.call_name.as_ref().map_or(0, String::capacity)
118			});
119		let references = index.references.estimated_heap_bytes()
120			+ index.references.iter().fold(0, |total, reference| {
121				total
122					+ reference.kind.capacity()
123					+ reference.call_name.as_ref().map_or(0, String::capacity)
124					+ reference.confidence.as_ref().map_or(0, String::capacity)
125					+ reference.receiver.as_ref().map_or(0, String::capacity)
126					+ reference.alias.as_ref().map_or(0, String::capacity)
127			}) + unique_arc_str_payload(
128			index
129				.references
130				.iter()
131				.map(|reference| &reference.target_identity),
132		);
133		size_of_val(index) + sources + symbols + references + index.inventory.estimated_heap_bytes()
134	}
135
136	pub fn from_linkage(linkage: &LinkageSnapshot) -> usize {
137		let mut bytes = size_of_val(linkage)
138			+ linkage.resolved.capacity() * size_of::<crate::snapshot::LinkageEdge>()
139			+ linkage.candidates.capacity() * size_of::<crate::snapshot::CandidateReference>()
140			+ linkage.external.capacity() * size_of::<crate::snapshot::ExternalReference>()
141			+ linkage.dynamic.capacity() * size_of::<crate::snapshot::DynamicReference>()
142			+ linkage.blocked.capacity() * size_of::<crate::snapshot::UnresolvedReference>()
143			+ linkage.manifest_blocked.capacity()
144				* size_of::<crate::snapshot::UnresolvedReference>()
145			+ linkage.unresolved.capacity() * size_of::<crate::snapshot::UnresolvedReference>()
146			+ linkage.read_index.estimated_heap_bytes();
147		bytes += linkage
148			.candidates
149			.iter()
150			.map(|candidate| candidate.targets.capacity() * size_of::<crate::snapshot::SymbolId>())
151			.sum::<usize>();
152		bytes += linkage
153			.dynamic
154			.iter()
155			.map(|reference| {
156				reference.candidates.capacity() * size_of::<crate::snapshot::SymbolId>()
157			})
158			.sum::<usize>();
159		bytes += unique_arc_str_payload(
160			linkage
161				.external
162				.iter()
163				.map(|reference| &reference.target_identity)
164				.chain(
165					linkage
166						.dynamic
167						.iter()
168						.map(|reference| &reference.target_identity),
169				)
170				.chain(
171					linkage
172						.blocked
173						.iter()
174						.map(|reference| &reference.target_identity),
175				)
176				.chain(
177					linkage
178						.manifest_blocked
179						.iter()
180						.map(|reference| &reference.target_identity),
181				)
182				.chain(
183					linkage
184						.unresolved
185						.iter()
186						.map(|reference| &reference.target_identity),
187				),
188		);
189		bytes
190	}
191}
192
193fn source_bytes(index: &CodeIndex) -> usize {
194	index
195		.sources
196		.iter()
197		.map(|source| {
198			if source.text.is_empty() {
199				std::fs::metadata(&source.path)
200					.ok()
201					.and_then(|metadata| usize::try_from(metadata.len()).ok())
202					.unwrap_or(0)
203			} else {
204				source.text.len()
205			}
206		})
207		.sum()
208}
209
210fn unique_arc_str_payload<'a>(values: impl Iterator<Item = &'a Arc<str>>) -> usize {
211	let mut seen = HashSet::<(usize, usize)>::new();
212	values
213		.filter(|value| seen.insert((value.as_ptr() as usize, value.len())))
214		.map(|value| value.len())
215		.sum()
216}
217
218fn definition_payload_bytes(definition: &DefRecord) -> usize {
219	definition.moniker.as_encoded().len()
220		+ definition.kind.len()
221		+ definition.visibility.len()
222		+ definition.signature.len()
223		+ definition.call_name.len()
224		+ definition.binding.len()
225		+ definition.origin.len()
226}
227
228fn reference_payload_bytes(reference: &RefRecord) -> usize {
229	reference.target.as_encoded().len()
230		+ reference.kind.len()
231		+ reference.receiver_hint.len()
232		+ reference.alias.len()
233		+ reference.confidence.len()
234		+ reference.call_name.len()
235		+ reference.binding.len()
236}
237
238#[cfg(test)]
239mod tests {
240	use super::SnapshotMemoryEstimate;
241	use crate::snapshot::{
242		CodeIndex, LinkageEdge, LinkageSnapshot, ReferenceId, ResourceGeneration, SymbolId,
243		SymbolRecord,
244	};
245
246	#[test]
247	fn estimates_grow_with_index_and_read_graph_storage() {
248		let generation = ResourceGeneration::new(1);
249		let empty_index = CodeIndex::new(generation, generation, Vec::new());
250		let populated_index = CodeIndex::new(
251			generation,
252			generation,
253			vec![SymbolRecord::new(
254				SymbolId::at(0, 0),
255				crate::snapshot::SourceId::at(0),
256				"main",
257				"function",
258			)],
259		);
260		assert!(
261			SnapshotMemoryEstimate::from_index(&populated_index)
262				> SnapshotMemoryEstimate::from_index(&empty_index)
263		);
264
265		let empty_graph = LinkageSnapshot::new(generation, generation, 0, 0);
266		let populated_graph = LinkageSnapshot::with_refs(
267			generation,
268			generation,
269			vec![LinkageEdge::new(ReferenceId::at(0, 0), SymbolId::at(0, 0))],
270			Vec::new(),
271		);
272		assert!(
273			SnapshotMemoryEstimate::from_linkage(&populated_graph)
274				> SnapshotMemoryEstimate::from_linkage(&empty_graph)
275		);
276	}
277}