1use serde::{Deserialize, Serialize};
4use std::collections::{BTreeMap, HashMap, HashSet};
5
6use super::key::debug_key;
7use super::store::AsyncStore;
8use super::AsyncProlly;
9use super::{child_cid_at, Cid, Error, Tree};
10#[cfg(test)]
11use super::{Prolly, Store};
12
13#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
15pub struct TreeDebugNode {
16 pub cid: Cid,
18 pub leaf: bool,
20 pub level: u8,
22 pub entry_count: usize,
24 pub max_entries: usize,
26 pub fill_factor: f64,
28 pub encoded_bytes: usize,
30 pub first_key: Option<Vec<u8>>,
32 pub last_key: Option<Vec<u8>>,
34}
35
36#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
38pub struct TreeDebugLevel {
39 pub level: u8,
41 pub nodes: Vec<TreeDebugNode>,
43}
44
45#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
47pub struct TreeDebugView {
48 pub levels: Vec<TreeDebugLevel>,
50}
51
52#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
54pub enum TreeDebugNodeStatus {
55 Shared,
57 LeftOnly,
59 RightOnly,
61}
62
63#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
65pub struct TreeDebugComparedNode {
66 pub status: TreeDebugNodeStatus,
68 pub node: TreeDebugNode,
70}
71
72#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
74pub struct TreeDebugComparisonLevel {
75 pub level: u8,
77 pub shared_nodes: usize,
79 pub left_only_nodes: usize,
81 pub right_only_nodes: usize,
83 pub shared_bytes: usize,
85 pub left_only_bytes: usize,
87 pub right_only_bytes: usize,
89 pub nodes: Vec<TreeDebugComparedNode>,
91}
92
93#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
95pub struct TreeDebugComparison {
96 pub shared_nodes: usize,
98 pub left_only_nodes: usize,
100 pub right_only_nodes: usize,
102 pub shared_bytes: usize,
104 pub left_only_bytes: usize,
106 pub right_only_bytes: usize,
108 pub levels: Vec<TreeDebugComparisonLevel>,
110}
111
112impl TreeDebugNode {
113 fn from_node(cid: Cid, node: &super::Node) -> Self {
114 let entry_count = node.len();
115 let max_entries = node.max_chunk_size();
116 let fill_factor = if max_entries == 0 {
117 0.0
118 } else {
119 entry_count as f64 / max_entries as f64
120 };
121
122 Self {
123 cid,
124 leaf: node.leaf,
125 level: node.level,
126 entry_count,
127 max_entries,
128 fill_factor,
129 encoded_bytes: node.encoded_len(),
130 first_key: node.keys.first().cloned(),
131 last_key: node.keys.last().cloned(),
132 }
133 }
134}
135
136impl TreeDebugView {
137 pub fn to_text(&self) -> String {
139 if self.levels.is_empty() {
140 return "empty tree".to_string();
141 }
142
143 let mut lines = Vec::new();
144 for (idx, level) in self.levels.iter().enumerate() {
145 let label = if idx == 0 { " (root)" } else { "" };
146 lines.push(format!(
147 "level {}{}: nodes={}",
148 level.level,
149 label,
150 level.nodes.len()
151 ));
152 for node in &level.nodes {
153 lines.push(format_node_line(" ", None, node));
154 }
155 }
156 lines.join("\n")
157 }
158}
159
160impl TreeDebugComparison {
161 pub fn to_text(&self) -> String {
163 if self.shared_nodes == 0 && self.left_only_nodes == 0 && self.right_only_nodes == 0 {
164 return "empty comparison".to_string();
165 }
166
167 let mut lines = vec![format!(
168 "shared={} ({} bytes), left_only={} ({} bytes), right_only={} ({} bytes)",
169 self.shared_nodes,
170 self.shared_bytes,
171 self.left_only_nodes,
172 self.left_only_bytes,
173 self.right_only_nodes,
174 self.right_only_bytes
175 )];
176
177 for level in &self.levels {
178 lines.push(format!(
179 "level {}: shared={} left_only={} right_only={}",
180 level.level, level.shared_nodes, level.left_only_nodes, level.right_only_nodes
181 ));
182 for compared in &level.nodes {
183 lines.push(format_node_line(
184 " ",
185 Some(compared.status.as_str()),
186 &compared.node,
187 ));
188 }
189 }
190
191 lines.join("\n")
192 }
193}
194
195impl TreeDebugNodeStatus {
196 fn as_str(&self) -> &'static str {
197 match self {
198 Self::Shared => "shared",
199 Self::LeftOnly => "left",
200 Self::RightOnly => "right",
201 }
202 }
203}
204
205#[cfg(test)]
206#[expect(
207 dead_code,
208 reason = "retained only as a correctness oracle for async diagnostics"
209)]
210pub(crate) fn collect_tree_debug_view<S: Store>(
211 prolly: &Prolly<S>,
212 tree: &Tree,
213) -> Result<TreeDebugView, Error> {
214 let Some(root_cid) = &tree.root else {
215 return Ok(TreeDebugView::default());
216 };
217
218 let mut grouped = BTreeMap::new();
219 let mut seen = HashSet::new();
220 let mut frontier = vec![root_cid.clone()];
221
222 while !frontier.is_empty() {
223 let nodes = prolly.load_many_ordered_with_parallelism(&frontier, 1)?;
224 let mut next_frontier = Vec::new();
225
226 for (cid, node) in frontier.iter().cloned().zip(nodes) {
227 if !seen.insert(cid.clone()) {
228 continue;
229 }
230 if node.keys.len() != node.vals.len() {
231 return Err(Error::InvalidNode);
232 }
233
234 grouped
235 .entry(node.level)
236 .or_insert_with(Vec::new)
237 .push(TreeDebugNode::from_node(cid, &node));
238
239 if !node.leaf {
240 next_frontier.reserve(node.vals.len());
241 for idx in 0..node.len() {
242 next_frontier.push(child_cid_at(&node, idx)?);
243 }
244 }
245 }
246
247 frontier = next_frontier;
248 }
249
250 Ok(view_from_grouped(grouped))
251}
252
253#[cfg(test)]
254#[expect(
255 dead_code,
256 reason = "retained only as a correctness oracle for async diagnostics"
257)]
258pub(crate) fn compare_tree_debug_views<S: Store>(
259 prolly: &Prolly<S>,
260 left: &Tree,
261 right: &Tree,
262) -> Result<TreeDebugComparison, Error> {
263 let left = collect_tree_debug_view(prolly, left)?;
264 let right = collect_tree_debug_view(prolly, right)?;
265 Ok(compare_views(left, right))
266}
267pub(crate) async fn collect_tree_debug_view_async<S>(
268 prolly: &AsyncProlly<S>,
269 tree: &Tree,
270) -> Result<TreeDebugView, Error>
271where
272 S: AsyncStore,
273 S::Error: Send + Sync,
274{
275 let Some(root_cid) = &tree.root else {
276 return Ok(TreeDebugView::default());
277 };
278
279 let mut grouped = BTreeMap::new();
280 let mut seen = HashSet::new();
281 let mut frontier = vec![root_cid.clone()];
282
283 while !frontier.is_empty() {
284 let nodes = prolly
285 .load_child_frontier_ordered_for_format(&frontier, &tree.config.format)
286 .await?;
287 let mut next_frontier = Vec::new();
288
289 for (cid, node) in frontier.iter().cloned().zip(nodes) {
290 if !seen.insert(cid.clone()) {
291 continue;
292 }
293 if node.keys.len() != node.vals.len() {
294 return Err(Error::InvalidNode);
295 }
296
297 grouped
298 .entry(node.level)
299 .or_insert_with(Vec::new)
300 .push(TreeDebugNode::from_node(cid, &node));
301
302 if !node.leaf {
303 next_frontier.reserve(node.vals.len());
304 for idx in 0..node.len() {
305 next_frontier.push(child_cid_at(&node, idx)?);
306 }
307 }
308 }
309
310 frontier = next_frontier;
311 }
312
313 Ok(view_from_grouped(grouped))
314}
315pub(crate) async fn compare_tree_debug_views_async<S>(
316 prolly: &AsyncProlly<S>,
317 left: &Tree,
318 right: &Tree,
319) -> Result<TreeDebugComparison, Error>
320where
321 S: AsyncStore,
322 S::Error: Send + Sync,
323{
324 let left = collect_tree_debug_view_async(prolly, left).await?;
325 let right = collect_tree_debug_view_async(prolly, right).await?;
326 Ok(compare_views(left, right))
327}
328
329fn view_from_grouped(grouped: BTreeMap<u8, Vec<TreeDebugNode>>) -> TreeDebugView {
330 let mut levels: Vec<_> = grouped
331 .into_iter()
332 .map(|(level, nodes)| TreeDebugLevel { level, nodes })
333 .collect();
334 levels.reverse();
335 TreeDebugView { levels }
336}
337
338fn compare_views(left: TreeDebugView, right: TreeDebugView) -> TreeDebugComparison {
339 let left_nodes = flatten_view(left);
340 let right_nodes = flatten_view(right);
341 let mut grouped: BTreeMap<u8, TreeDebugComparisonLevel> = BTreeMap::new();
342 let mut comparison = TreeDebugComparison::default();
343
344 let mut left_cids: Vec<_> = left_nodes.keys().cloned().collect();
345 left_cids.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
346
347 for cid in left_cids {
348 let node = left_nodes
349 .get(&cid)
350 .expect("CID collected from map keys must exist")
351 .clone();
352 if right_nodes.contains_key(&cid) {
353 comparison.shared_nodes += 1;
354 comparison.shared_bytes += node.encoded_bytes;
355 push_compared_node(&mut grouped, TreeDebugNodeStatus::Shared, node);
356 } else {
357 comparison.left_only_nodes += 1;
358 comparison.left_only_bytes += node.encoded_bytes;
359 push_compared_node(&mut grouped, TreeDebugNodeStatus::LeftOnly, node);
360 }
361 }
362
363 let mut right_cids: Vec<_> = right_nodes.keys().cloned().collect();
364 right_cids.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
365
366 for cid in right_cids {
367 if left_nodes.contains_key(&cid) {
368 continue;
369 }
370 let node = right_nodes
371 .get(&cid)
372 .expect("CID collected from map keys must exist")
373 .clone();
374 comparison.right_only_nodes += 1;
375 comparison.right_only_bytes += node.encoded_bytes;
376 push_compared_node(&mut grouped, TreeDebugNodeStatus::RightOnly, node);
377 }
378
379 comparison.levels = grouped.into_values().collect();
380 comparison.levels.reverse();
381 comparison
382}
383
384fn flatten_view(view: TreeDebugView) -> HashMap<Cid, TreeDebugNode> {
385 view.levels
386 .into_iter()
387 .flat_map(|level| level.nodes)
388 .map(|node| (node.cid.clone(), node))
389 .collect()
390}
391
392fn push_compared_node(
393 grouped: &mut BTreeMap<u8, TreeDebugComparisonLevel>,
394 status: TreeDebugNodeStatus,
395 node: TreeDebugNode,
396) {
397 let level = grouped
398 .entry(node.level)
399 .or_insert_with(|| TreeDebugComparisonLevel {
400 level: node.level,
401 ..TreeDebugComparisonLevel::default()
402 });
403
404 match status {
405 TreeDebugNodeStatus::Shared => {
406 level.shared_nodes += 1;
407 level.shared_bytes += node.encoded_bytes;
408 }
409 TreeDebugNodeStatus::LeftOnly => {
410 level.left_only_nodes += 1;
411 level.left_only_bytes += node.encoded_bytes;
412 }
413 TreeDebugNodeStatus::RightOnly => {
414 level.right_only_nodes += 1;
415 level.right_only_bytes += node.encoded_bytes;
416 }
417 }
418
419 level.nodes.push(TreeDebugComparedNode { status, node });
420 level.nodes.sort_by(|a, b| {
421 status_rank(&a.status)
422 .cmp(&status_rank(&b.status))
423 .then_with(|| a.node.cid.as_bytes().cmp(b.node.cid.as_bytes()))
424 });
425}
426
427fn status_rank(status: &TreeDebugNodeStatus) -> u8 {
428 match status {
429 TreeDebugNodeStatus::Shared => 0,
430 TreeDebugNodeStatus::LeftOnly => 1,
431 TreeDebugNodeStatus::RightOnly => 2,
432 }
433}
434
435fn format_node_line(prefix: &str, status: Option<&str>, node: &TreeDebugNode) -> String {
436 let kind = if node.leaf { "L" } else { "I" };
437 let status = status.map(|value| format!("{value} ")).unwrap_or_default();
438 format!(
439 "{prefix}{status}{kind} {} entries={}/{} fill={:.1}% bytes={} keys={}..{}",
440 short_cid(&node.cid),
441 node.entry_count,
442 node.max_entries,
443 node.fill_factor * 100.0,
444 node.encoded_bytes,
445 format_optional_key(node.first_key.as_deref()),
446 format_optional_key(node.last_key.as_deref())
447 )
448}
449
450fn format_optional_key(key: Option<&[u8]>) -> String {
451 key.map(debug_key).unwrap_or_else(|| "-".to_string())
452}
453
454fn short_cid(cid: &Cid) -> String {
455 const HEX: &[u8; 16] = b"0123456789abcdef";
456 let mut out = String::with_capacity(12);
457 for byte in cid.as_bytes().iter().take(6) {
458 out.push(HEX[(byte >> 4) as usize] as char);
459 out.push(HEX[(byte & 0x0f) as usize] as char);
460 }
461 out
462}