1use serde::{Deserialize, Serialize};
4use std::collections::{BTreeMap, HashMap, HashSet};
5
6use super::key::debug_key;
7#[cfg(feature = "async-store")]
8use super::store::AsyncStore;
9#[cfg(feature = "async-store")]
10use super::AsyncProlly;
11use super::{child_cid_at, Cid, Error, Prolly, Store, Tree};
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
205pub(crate) fn collect_tree_debug_view<S: Store>(
206 prolly: &Prolly<S>,
207 tree: &Tree,
208) -> Result<TreeDebugView, Error> {
209 let Some(root_cid) = &tree.root else {
210 return Ok(TreeDebugView::default());
211 };
212
213 let mut grouped = BTreeMap::new();
214 let mut seen = HashSet::new();
215 let mut frontier = vec![root_cid.clone()];
216
217 while !frontier.is_empty() {
218 let nodes = prolly.load_many_ordered_with_parallelism(&frontier, 1)?;
219 let mut next_frontier = Vec::new();
220
221 for (cid, node) in frontier.iter().cloned().zip(nodes) {
222 if !seen.insert(cid.clone()) {
223 continue;
224 }
225 if node.keys.len() != node.vals.len() {
226 return Err(Error::InvalidNode);
227 }
228
229 grouped
230 .entry(node.level)
231 .or_insert_with(Vec::new)
232 .push(TreeDebugNode::from_node(cid, &node));
233
234 if !node.leaf {
235 next_frontier.reserve(node.vals.len());
236 for idx in 0..node.len() {
237 next_frontier.push(child_cid_at(&node, idx)?);
238 }
239 }
240 }
241
242 frontier = next_frontier;
243 }
244
245 Ok(view_from_grouped(grouped))
246}
247
248pub(crate) fn compare_tree_debug_views<S: Store>(
249 prolly: &Prolly<S>,
250 left: &Tree,
251 right: &Tree,
252) -> Result<TreeDebugComparison, Error> {
253 let left = collect_tree_debug_view(prolly, left)?;
254 let right = collect_tree_debug_view(prolly, right)?;
255 Ok(compare_views(left, right))
256}
257
258#[cfg(feature = "async-store")]
259pub(crate) async fn collect_tree_debug_view_async<S>(
260 prolly: &AsyncProlly<S>,
261 tree: &Tree,
262) -> Result<TreeDebugView, Error>
263where
264 S: AsyncStore,
265 S::Error: Send + Sync,
266{
267 let Some(root_cid) = &tree.root else {
268 return Ok(TreeDebugView::default());
269 };
270
271 let mut grouped = BTreeMap::new();
272 let mut seen = HashSet::new();
273 let mut frontier = vec![root_cid.clone()];
274
275 while !frontier.is_empty() {
276 let nodes = prolly.load_child_frontier_ordered(&frontier).await?;
277 let mut next_frontier = Vec::new();
278
279 for (cid, node) in frontier.iter().cloned().zip(nodes) {
280 if !seen.insert(cid.clone()) {
281 continue;
282 }
283 if node.keys.len() != node.vals.len() {
284 return Err(Error::InvalidNode);
285 }
286
287 grouped
288 .entry(node.level)
289 .or_insert_with(Vec::new)
290 .push(TreeDebugNode::from_node(cid, &node));
291
292 if !node.leaf {
293 next_frontier.reserve(node.vals.len());
294 for idx in 0..node.len() {
295 next_frontier.push(child_cid_at(&node, idx)?);
296 }
297 }
298 }
299
300 frontier = next_frontier;
301 }
302
303 Ok(view_from_grouped(grouped))
304}
305
306#[cfg(feature = "async-store")]
307pub(crate) async fn compare_tree_debug_views_async<S>(
308 prolly: &AsyncProlly<S>,
309 left: &Tree,
310 right: &Tree,
311) -> Result<TreeDebugComparison, Error>
312where
313 S: AsyncStore,
314 S::Error: Send + Sync,
315{
316 let left = collect_tree_debug_view_async(prolly, left).await?;
317 let right = collect_tree_debug_view_async(prolly, right).await?;
318 Ok(compare_views(left, right))
319}
320
321fn view_from_grouped(grouped: BTreeMap<u8, Vec<TreeDebugNode>>) -> TreeDebugView {
322 let mut levels: Vec<_> = grouped
323 .into_iter()
324 .map(|(level, nodes)| TreeDebugLevel { level, nodes })
325 .collect();
326 levels.reverse();
327 TreeDebugView { levels }
328}
329
330fn compare_views(left: TreeDebugView, right: TreeDebugView) -> TreeDebugComparison {
331 let left_nodes = flatten_view(left);
332 let right_nodes = flatten_view(right);
333 let mut grouped: BTreeMap<u8, TreeDebugComparisonLevel> = BTreeMap::new();
334 let mut comparison = TreeDebugComparison::default();
335
336 let mut left_cids: Vec<_> = left_nodes.keys().cloned().collect();
337 left_cids.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
338
339 for cid in left_cids {
340 let node = left_nodes
341 .get(&cid)
342 .expect("CID collected from map keys must exist")
343 .clone();
344 if right_nodes.contains_key(&cid) {
345 comparison.shared_nodes += 1;
346 comparison.shared_bytes += node.encoded_bytes;
347 push_compared_node(&mut grouped, TreeDebugNodeStatus::Shared, node);
348 } else {
349 comparison.left_only_nodes += 1;
350 comparison.left_only_bytes += node.encoded_bytes;
351 push_compared_node(&mut grouped, TreeDebugNodeStatus::LeftOnly, node);
352 }
353 }
354
355 let mut right_cids: Vec<_> = right_nodes.keys().cloned().collect();
356 right_cids.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
357
358 for cid in right_cids {
359 if left_nodes.contains_key(&cid) {
360 continue;
361 }
362 let node = right_nodes
363 .get(&cid)
364 .expect("CID collected from map keys must exist")
365 .clone();
366 comparison.right_only_nodes += 1;
367 comparison.right_only_bytes += node.encoded_bytes;
368 push_compared_node(&mut grouped, TreeDebugNodeStatus::RightOnly, node);
369 }
370
371 comparison.levels = grouped.into_values().collect();
372 comparison.levels.reverse();
373 comparison
374}
375
376fn flatten_view(view: TreeDebugView) -> HashMap<Cid, TreeDebugNode> {
377 view.levels
378 .into_iter()
379 .flat_map(|level| level.nodes)
380 .map(|node| (node.cid.clone(), node))
381 .collect()
382}
383
384fn push_compared_node(
385 grouped: &mut BTreeMap<u8, TreeDebugComparisonLevel>,
386 status: TreeDebugNodeStatus,
387 node: TreeDebugNode,
388) {
389 let level = grouped
390 .entry(node.level)
391 .or_insert_with(|| TreeDebugComparisonLevel {
392 level: node.level,
393 ..TreeDebugComparisonLevel::default()
394 });
395
396 match status {
397 TreeDebugNodeStatus::Shared => {
398 level.shared_nodes += 1;
399 level.shared_bytes += node.encoded_bytes;
400 }
401 TreeDebugNodeStatus::LeftOnly => {
402 level.left_only_nodes += 1;
403 level.left_only_bytes += node.encoded_bytes;
404 }
405 TreeDebugNodeStatus::RightOnly => {
406 level.right_only_nodes += 1;
407 level.right_only_bytes += node.encoded_bytes;
408 }
409 }
410
411 level.nodes.push(TreeDebugComparedNode { status, node });
412 level.nodes.sort_by(|a, b| {
413 status_rank(&a.status)
414 .cmp(&status_rank(&b.status))
415 .then_with(|| a.node.cid.as_bytes().cmp(b.node.cid.as_bytes()))
416 });
417}
418
419fn status_rank(status: &TreeDebugNodeStatus) -> u8 {
420 match status {
421 TreeDebugNodeStatus::Shared => 0,
422 TreeDebugNodeStatus::LeftOnly => 1,
423 TreeDebugNodeStatus::RightOnly => 2,
424 }
425}
426
427fn format_node_line(prefix: &str, status: Option<&str>, node: &TreeDebugNode) -> String {
428 let kind = if node.leaf { "L" } else { "I" };
429 let status = status.map(|value| format!("{value} ")).unwrap_or_default();
430 format!(
431 "{prefix}{status}{kind} {} entries={}/{} fill={:.1}% bytes={} keys={}..{}",
432 short_cid(&node.cid),
433 node.entry_count,
434 node.max_entries,
435 node.fill_factor * 100.0,
436 node.encoded_bytes,
437 format_optional_key(node.first_key.as_deref()),
438 format_optional_key(node.last_key.as_deref())
439 )
440}
441
442fn format_optional_key(key: Option<&[u8]>) -> String {
443 key.map(debug_key).unwrap_or_else(|| "-".to_string())
444}
445
446fn short_cid(cid: &Cid) -> String {
447 const HEX: &[u8; 16] = b"0123456789abcdef";
448 let mut out = String::with_capacity(12);
449 for byte in cid.as_bytes().iter().take(6) {
450 out.push(HEX[(byte >> 4) as usize] as char);
451 out.push(HEX[(byte & 0x0f) as usize] as char);
452 }
453 out
454}