Files
bft-crdt-experiment/crates/bft-json-crdt/src/debug.rs

318 lines
9.2 KiB
Rust
Raw Normal View History

use crate::{
json_crdt::{BaseCrdt, CrdtNode, SignedOp},
keypair::SignedDigest,
list_crdt::ListCrdt,
op::{Op, OpId, PathSegment},
};
#[cfg(feature = "logging-base")]
use {
crate::{
keypair::{lsb_32, AuthorId},
op::{print_hex, print_path, ROOT_ID},
},
colored::Colorize,
random_color::{Luminosity, RandomColor},
};
#[cfg(feature = "logging-list")]
use std::collections::HashMap;
use std::fmt::Display;
#[cfg(feature = "logging-base")]
fn author_to_hex(author: AuthorId) -> String {
format!("{:#010x}", lsb_32(author))
}
#[cfg(feature = "logging-base")]
fn display_op_id<T: CrdtNode>(op: &Op<T>) -> String {
let [r, g, b] = RandomColor::new()
.luminosity(Luminosity::Light)
.seed(lsb_32(op.author))
.to_rgb_array();
format!(
"[{},{}]",
author_to_hex(op.author).bold().truecolor(r, g, b),
op.seq.to_string().yellow()
)
}
pub fn debug_type_mismatch(_msg: String) {
#[cfg(feature = "logging-base")]
{
println!(" {}\n {_msg}", "type mismatch! ignoring this node".red(),);
}
}
pub fn debug_path_mismatch(_our_path: Vec<PathSegment>, _op_path: Vec<PathSegment>) {
#[cfg(feature = "logging-base")]
{
println!(
" {}\n current path: {}\n op path: {}",
"path mismatch!".red(),
print_path(_our_path),
print_path(_op_path),
);
}
}
pub fn debug_op_on_primitive(_op_path: Vec<PathSegment>) {
#[cfg(feature = "logging-base")]
{
println!(
" {} this is an error, ignoring op.\n op path: {}",
"trying to apply() on a primitive!".red(),
print_path(_op_path),
);
}
}
#[cfg(feature = "logging-base")]
fn display_author(author: AuthorId) -> String {
let [r, g, b] = RandomColor::new()
.luminosity(Luminosity::Light)
.seed(lsb_32(author))
.to_rgb_array();
format!(" {} ", author_to_hex(author))
.black()
.on_truecolor(r, g, b)
.to_string()
}
pub trait DebugView {
fn debug_view(&self, indent: usize) -> String;
}
impl<T: CrdtNode + DebugView> BaseCrdt<T> {
pub fn debug_view(&self) {
#[cfg(feature = "logging-json")]
println!("document is now:\n{}", self.doc.debug_view(0));
}
pub fn log_try_apply(&self, _op: &SignedOp) {
#[cfg(feature = "logging-json")]
println!(
"{} trying to apply operation {} from {}",
display_author(self.id),
&print_hex(&_op.signed_digest)[..6],
display_author(_op.inner.author())
);
}
pub fn debug_digest_failure(&self, _op: SignedOp) {
#[cfg(feature = "logging-json")]
println!(
" {} cannot confirm signed_digest from {}",
"digest failure!".red(),
display_author(_op.author())
);
}
pub fn log_missing_causal_dep(&self, _missing: &SignedDigest) {
#[cfg(feature = "logging-json")]
println!(
" {} haven't received op with digest {}",
"missing causal dependency".red(),
print_hex(_missing)
);
}
pub fn log_actually_apply(&self, _op: &SignedOp) {
#[cfg(feature = "logging-json")]
{
println!(
" applying op to path: /{}",
print_path(_op.inner.path.clone())
);
println!("{}", _op.inner.debug_view(2));
}
}
}
impl<T> Op<T>
where
T: CrdtNode,
{
pub fn debug_hash_failure(&self) {
#[cfg(feature = "logging-base")]
{
println!(" {}", "hash failure!".red());
println!(" expected: {}", print_hex(&self.id));
println!(" computed: {}", print_hex(&self.hash_to_id()));
}
}
}
impl<T> DebugView for T
where
T: Display,
{
#[cfg(feature = "logging-base")]
fn debug_view(&self, _indent: usize) -> String {
self.to_string()
}
#[cfg(not(feature = "logging-base"))]
fn debug_view(&self, _indent: usize) -> String {
"".to_string()
}
}
impl<T> DebugView for Op<T>
where
T: DebugView + CrdtNode,
{
#[cfg(not(feature = "logging-base"))]
fn debug_view(&self, _indent: usize) -> String {
"".to_string()
}
#[cfg(feature = "logging-json")]
fn debug_view(&self, indent: usize) -> String {
let op_id = display_op_id(self);
let content = if self.id == ROOT_ID && self.content.is_none() {
"root".blue().bold().to_string()
} else {
self.content
.as_ref()
.map_or("[empty]".to_string(), |c| c.debug_view(indent + 2))
};
let content_str = if self.is_deleted && self.id != ROOT_ID {
content.red().strikethrough().to_string()
} else {
content
};
format!("{op_id} {content_str}")
}
}
impl<T> ListCrdt<T>
where
T: CrdtNode,
{
pub fn log_ops(&self, highlight: Option<OpId>) {
#[cfg(feature = "logging-list")]
{
let mut lines = Vec::<String>::new();
// do in-order traversal
let res: Vec<&Op<T>> = self.ops.iter().collect();
if res.is_empty() {
println!("[empty]");
}
// figure out parent-child hierarchies from origins
let mut parent_child_map: HashMap<OpId, Vec<OpId>> = HashMap::new();
for op in &res {
let children = parent_child_map.entry(op.origin).or_default();
children.push(op.id);
}
let is_last = |op: &Op<T>| -> bool {
if op.id == ROOT_ID {
return true;
}
if let Some(children) = parent_child_map.get(&op.origin) {
return *children.last().unwrap() == op.id;
}
false
};
// make stack of origins
let mut stack: Vec<(OpId, &str)> = Vec::new();
stack.push((ROOT_ID, ""));
let mut prev = None;
for op in &res {
let origin_idx = self.find_idx(op.origin).unwrap();
let origin = &res[origin_idx];
let origin_id = origin.id;
if let Some(prev) = prev {
if origin_id == prev {
// went down one layer, add to stack
let stack_prefix_char = if is_last(origin) { " " } else { "" };
stack.push((prev, stack_prefix_char));
}
}
// pop back up until we reach the right origin
while stack.last().unwrap().0 != origin_id {
stack.pop();
}
let cur_char = if is_last(op) { "╰─" } else { "├─" };
let prefixes = stack.iter().map(|s| s.1).collect::<Vec<_>>().join("");
let highlight_text = if highlight.is_some() && highlight.unwrap() == op.id {
if op.is_deleted {
"<- deleted".bold().red()
} else {
"<- inserted".bold().green()
}
.to_string()
} else {
"".to_string()
};
let content = if op.id == ROOT_ID {
"root".blue().bold().to_string()
} else {
op.content
.as_ref()
.map_or("[empty]".to_string(), |c| c.hash())
};
if op.is_deleted && op.id != ROOT_ID {
lines.push(format!(
"{}{}{} {} {}",
prefixes,
cur_char,
display_op_id(op),
content.strikethrough().red(),
highlight_text
));
} else {
lines.push(format!(
"{}{}{} {} {}",
prefixes,
cur_char,
display_op_id(op),
content,
highlight_text
));
}
prev = Some(op.id);
}
// full string
let flat = self.iter().map(|t| t.hash()).collect::<Vec<_>>().join("");
lines.push(format!("Flattened result: {}", flat));
println!("{}", lines.join("\n"));
}
}
pub fn log_apply(&self, op: &Op<T>) {
#[cfg(feature = "logging-list")]
{
if op.is_deleted {
println!(
"{} Performing a delete of {}@{}",
display_author(self.our_id),
display_op_id(op),
op.sequence_num(),
);
return;
}
if let Some(content) = op.content.as_ref() {
println!(
"{} Performing an insert of {}@{}: '{}' after {}",
display_author(self.our_id),
display_op_id(op),
op.sequence_num(),
content.hash(),
display_op_id(op)
);
}
}
}
}