mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-27 18:58:10 -07:00
global: snapshot
This commit is contained in:
@@ -120,6 +120,18 @@ pub fn find_common_suffix(names: &[&str]) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Normalize a prefix string by ensuring it ends with underscore.
|
||||
/// Returns empty string if input is empty.
|
||||
pub fn normalize_prefix(s: &str) -> String {
|
||||
if s.is_empty() {
|
||||
String::new()
|
||||
} else if s.ends_with('_') {
|
||||
s.to_string()
|
||||
} else {
|
||||
format!("{}_", s)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::collections::{BTreeSet, HashMap};
|
||||
use brk_types::{TreeNode, extract_json_type};
|
||||
|
||||
use super::analyze_pattern_modes;
|
||||
use crate::{PatternField, StructuralPattern, to_pascal_case};
|
||||
use crate::{PatternBaseResult, PatternField, StructuralPattern, to_pascal_case};
|
||||
|
||||
/// Context for pattern detection, holding all intermediate state.
|
||||
struct PatternContext {
|
||||
@@ -38,14 +38,16 @@ impl PatternContext {
|
||||
|
||||
/// Detect structural patterns in the tree using a bottom-up approach.
|
||||
///
|
||||
/// Returns (patterns, concrete_to_pattern, concrete_to_type_param).
|
||||
/// Returns (patterns, concrete_to_pattern, concrete_to_type_param, node_bases).
|
||||
/// Each pattern has its `mode` set based on analysis of all instances.
|
||||
/// `node_bases` maps tree paths to their computed PatternBaseResult for use during generation.
|
||||
pub fn detect_structural_patterns(
|
||||
tree: &TreeNode,
|
||||
) -> (
|
||||
Vec<StructuralPattern>,
|
||||
HashMap<Vec<PatternField>, String>,
|
||||
HashMap<Vec<PatternField>, String>,
|
||||
HashMap<String, PatternBaseResult>,
|
||||
) {
|
||||
let mut ctx = PatternContext::new();
|
||||
resolve_branch_patterns(tree, "root", &mut ctx);
|
||||
@@ -99,10 +101,11 @@ pub fn detect_structural_patterns(
|
||||
let concrete_to_pattern = pattern_lookup.clone();
|
||||
|
||||
// Analyze pattern modes (suffix vs prefix) from all instances
|
||||
analyze_pattern_modes(tree, &mut patterns, &pattern_lookup);
|
||||
// Also collects node bases for each tree path
|
||||
let node_bases = analyze_pattern_modes(tree, &mut patterns, &pattern_lookup);
|
||||
|
||||
patterns.sort_by(|a, b| b.fields.len().cmp(&a.fields.len()));
|
||||
(patterns, concrete_to_pattern, type_mappings)
|
||||
(patterns, concrete_to_pattern, type_mappings, node_bases)
|
||||
}
|
||||
|
||||
/// Detect generic patterns by grouping signatures by their normalized form.
|
||||
|
||||
@@ -8,8 +8,8 @@ use std::collections::HashMap;
|
||||
|
||||
use brk_types::TreeNode;
|
||||
|
||||
use super::{find_common_prefix, find_common_suffix, get_node_fields};
|
||||
use crate::{PatternField, PatternMode, StructuralPattern};
|
||||
use super::{find_common_prefix, find_common_suffix, get_node_fields, normalize_prefix};
|
||||
use crate::{PatternBaseResult, PatternField, PatternMode, StructuralPattern, build_child_path};
|
||||
|
||||
/// Result of analyzing a single pattern instance.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -28,16 +28,21 @@ struct InstanceAnalysis {
|
||||
/// This is the main entry point for mode detection. It processes
|
||||
/// the tree bottom-up, collecting analysis for each pattern instance,
|
||||
/// then determines the consistent mode for each pattern.
|
||||
///
|
||||
/// Returns a map from tree paths to their computed PatternBaseResult.
|
||||
/// This map is used during generation to check pattern compatibility.
|
||||
pub fn analyze_pattern_modes(
|
||||
tree: &TreeNode,
|
||||
patterns: &mut [StructuralPattern],
|
||||
pattern_lookup: &HashMap<Vec<PatternField>, String>,
|
||||
) {
|
||||
) -> HashMap<String, PatternBaseResult> {
|
||||
// Collect analyses from all instances, keyed by pattern name
|
||||
let mut all_analyses: HashMap<String, Vec<InstanceAnalysis>> = HashMap::new();
|
||||
// Also collect base results for each node, keyed by tree path
|
||||
let mut node_bases: HashMap<String, PatternBaseResult> = HashMap::new();
|
||||
|
||||
// Bottom-up traversal
|
||||
collect_instance_analyses(tree, pattern_lookup, &mut all_analyses);
|
||||
collect_instance_analyses(tree, "", pattern_lookup, &mut all_analyses, &mut node_bases);
|
||||
|
||||
// For each pattern, determine mode from collected instances
|
||||
for pattern in patterns.iter_mut() {
|
||||
@@ -45,14 +50,20 @@ pub fn analyze_pattern_modes(
|
||||
pattern.mode = determine_pattern_mode(analyses, &pattern.fields);
|
||||
}
|
||||
}
|
||||
|
||||
node_bases
|
||||
}
|
||||
|
||||
/// Recursively collect instance analyses bottom-up.
|
||||
/// Returns the "base" for this node (used by parent for its analysis).
|
||||
///
|
||||
/// Also stores the PatternBaseResult for each node in `node_bases`, keyed by path.
|
||||
fn collect_instance_analyses(
|
||||
node: &TreeNode,
|
||||
path: &str,
|
||||
pattern_lookup: &HashMap<Vec<PatternField>, String>,
|
||||
all_analyses: &mut HashMap<String, Vec<InstanceAnalysis>>,
|
||||
node_bases: &mut HashMap<String, PatternBaseResult>,
|
||||
) -> Option<String> {
|
||||
match node {
|
||||
TreeNode::Leaf(leaf) => {
|
||||
@@ -63,9 +74,14 @@ fn collect_instance_analyses(
|
||||
// First, process all children recursively (bottom-up)
|
||||
let mut child_bases: HashMap<String, String> = HashMap::new();
|
||||
for (field_name, child_node) in children {
|
||||
if let Some(base) =
|
||||
collect_instance_analyses(child_node, pattern_lookup, all_analyses)
|
||||
{
|
||||
let child_path = build_child_path(path, field_name);
|
||||
if let Some(base) = collect_instance_analyses(
|
||||
child_node,
|
||||
&child_path,
|
||||
pattern_lookup,
|
||||
all_analyses,
|
||||
node_bases,
|
||||
) {
|
||||
child_bases.insert(field_name.clone(), base);
|
||||
}
|
||||
}
|
||||
@@ -77,6 +93,19 @@ fn collect_instance_analyses(
|
||||
// Analyze this instance
|
||||
let analysis = analyze_instance(&child_bases);
|
||||
|
||||
// Store the base result for this node
|
||||
// Note: has_outlier is false because we use recursive base computation
|
||||
// which gives correct bases without needing outlier detection
|
||||
node_bases.insert(
|
||||
path.to_string(),
|
||||
PatternBaseResult {
|
||||
base: analysis.base.clone(),
|
||||
has_outlier: false,
|
||||
is_suffix_mode: analysis.is_suffix_mode,
|
||||
field_parts: analysis.field_parts.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
// Get the pattern name for this node (if any)
|
||||
let fields = get_node_fields(children, pattern_lookup);
|
||||
if let Some(pattern_name) = pattern_lookup.get(&fields) {
|
||||
@@ -128,19 +157,10 @@ fn analyze_instance(child_bases: &HashMap<String, String>) -> InstanceAnalysis {
|
||||
let mut field_parts = HashMap::new();
|
||||
|
||||
for (field_name, child_base) in child_bases {
|
||||
// Prefix = child_base with common suffix stripped
|
||||
// Prefix = child_base with common suffix stripped, normalized to end with _
|
||||
let prefix = child_base
|
||||
.strip_suffix(&common_suffix)
|
||||
.map(|s| {
|
||||
// Ensure prefix ends with underscore if non-empty
|
||||
if s.is_empty() {
|
||||
String::new()
|
||||
} else if s.ends_with('_') {
|
||||
s.to_string()
|
||||
} else {
|
||||
format!("{}_", s)
|
||||
}
|
||||
})
|
||||
.map(normalize_prefix)
|
||||
.unwrap_or_default();
|
||||
field_parts.insert(field_name.clone(), prefix);
|
||||
}
|
||||
@@ -152,16 +172,16 @@ fn analyze_instance(child_bases: &HashMap<String, String>) -> InstanceAnalysis {
|
||||
};
|
||||
}
|
||||
|
||||
// No common prefix or suffix - use first child's base and treat as suffix mode
|
||||
// with full metric names as relatives
|
||||
let base = child_bases.values().next().cloned().unwrap_or_default();
|
||||
// No common prefix or suffix - use empty base so _m(base, relative) returns just the relative.
|
||||
// This handles cases like utxo_cohorts.all.activity where children have completely
|
||||
// different bases (coinblocks_destroyed, coindays_destroyed, etc.)
|
||||
let field_parts = child_bases
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect();
|
||||
|
||||
InstanceAnalysis {
|
||||
base,
|
||||
base: String::new(),
|
||||
field_parts,
|
||||
is_suffix_mode: true,
|
||||
}
|
||||
@@ -197,7 +217,9 @@ fn determine_pattern_mode(
|
||||
// Convert to sorted Vec for comparison since HashMap isn't hashable
|
||||
let mut parts_counts: HashMap<Vec<(String, String)>, usize> = HashMap::new();
|
||||
for analysis in &majority_instances {
|
||||
let mut sorted: Vec<_> = analysis.field_parts.iter()
|
||||
let mut sorted: Vec<_> = analysis
|
||||
.field_parts
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect();
|
||||
sorted.sort();
|
||||
@@ -244,7 +266,10 @@ mod tests {
|
||||
assert_eq!(analysis.base, "lth_cost_basis");
|
||||
assert_eq!(analysis.field_parts.get("max"), Some(&"max".to_string()));
|
||||
assert_eq!(analysis.field_parts.get("min"), Some(&"min".to_string()));
|
||||
assert_eq!(analysis.field_parts.get("percentiles"), Some(&"".to_string()));
|
||||
assert_eq!(
|
||||
analysis.field_parts.get("percentiles"),
|
||||
Some(&"".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -280,7 +305,10 @@ mod tests {
|
||||
assert_eq!(analysis.base, "cost_basis");
|
||||
assert_eq!(analysis.field_parts.get("max"), Some(&"max".to_string()));
|
||||
assert_eq!(analysis.field_parts.get("min"), Some(&"min".to_string()));
|
||||
assert_eq!(analysis.field_parts.get("percentiles"), Some(&"".to_string()));
|
||||
assert_eq!(
|
||||
analysis.field_parts.get("percentiles"),
|
||||
Some(&"".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -9,15 +9,7 @@ use brk_types::{Index, TreeNode, extract_json_type};
|
||||
|
||||
use crate::{IndexSetPattern, PatternField, child_type_name};
|
||||
|
||||
use super::{find_common_prefix, find_common_suffix};
|
||||
|
||||
/// Get the first leaf name from a tree node.
|
||||
pub fn get_first_leaf_name(node: &TreeNode) -> Option<String> {
|
||||
match node {
|
||||
TreeNode::Leaf(leaf) => Some(leaf.name().to_string()),
|
||||
TreeNode::Branch(children) => children.values().find_map(get_first_leaf_name),
|
||||
}
|
||||
}
|
||||
use super::{find_common_prefix, find_common_suffix, normalize_prefix};
|
||||
|
||||
/// Get the shortest leaf name from a tree node.
|
||||
///
|
||||
@@ -128,6 +120,30 @@ pub struct PatternBaseResult {
|
||||
pub field_parts: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl PatternBaseResult {
|
||||
/// Create a default result that forces inlining (has_outlier = true).
|
||||
/// Use when no pattern base could be computed during lookup.
|
||||
pub fn force_inline() -> Self {
|
||||
Self {
|
||||
base: String::new(),
|
||||
has_outlier: true,
|
||||
is_suffix_mode: true,
|
||||
field_parts: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an empty result with no outlier.
|
||||
/// Use for root-level patterns or when children have no common pattern.
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
base: String::new(),
|
||||
has_outlier: false,
|
||||
is_suffix_mode: true,
|
||||
field_parts: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the metric base for a pattern instance by analyzing direct children.
|
||||
///
|
||||
/// Uses the shortest leaf names from direct children to find common prefix/suffix.
|
||||
@@ -140,12 +156,7 @@ pub struct PatternBaseResult {
|
||||
pub fn get_pattern_instance_base(node: &TreeNode) -> PatternBaseResult {
|
||||
let child_names = get_direct_children_for_analysis(node);
|
||||
if child_names.is_empty() {
|
||||
return PatternBaseResult {
|
||||
base: String::new(),
|
||||
has_outlier: false,
|
||||
is_suffix_mode: true, // default
|
||||
field_parts: HashMap::new(),
|
||||
};
|
||||
return PatternBaseResult::empty();
|
||||
}
|
||||
|
||||
// Try to find common base from leaf names
|
||||
@@ -181,12 +192,7 @@ pub fn get_pattern_instance_base(node: &TreeNode) -> PatternBaseResult {
|
||||
|
||||
// Fallback: no common prefix/suffix found - this is a root-level pattern
|
||||
// Return empty base so metric names are used directly
|
||||
PatternBaseResult {
|
||||
base: String::new(),
|
||||
has_outlier: false,
|
||||
is_suffix_mode: true, // default
|
||||
field_parts: HashMap::new(),
|
||||
}
|
||||
PatternBaseResult::empty()
|
||||
}
|
||||
|
||||
/// Result of try_find_base: base name, has_outlier flag, is_suffix_mode flag, and field_parts.
|
||||
@@ -199,7 +205,10 @@ struct FindBaseResult {
|
||||
|
||||
/// Try to find a common base from child names using prefix/suffix detection.
|
||||
/// Returns Some(FindBaseResult) if found.
|
||||
fn try_find_base(child_names: &[(String, String)], is_outlier_attempt: bool) -> Option<FindBaseResult> {
|
||||
fn try_find_base(
|
||||
child_names: &[(String, String)],
|
||||
is_outlier_attempt: bool,
|
||||
) -> Option<FindBaseResult> {
|
||||
let leaf_names: Vec<&str> = child_names.iter().map(|(_, n)| n.as_str()).collect();
|
||||
|
||||
// Try common prefix first (suffix mode)
|
||||
@@ -231,18 +240,10 @@ fn try_find_base(child_names: &[(String, String)], is_outlier_attempt: bool) ->
|
||||
let base = suffix.trim_start_matches('_').to_string();
|
||||
let mut field_parts = HashMap::new();
|
||||
for (field_name, leaf_name) in child_names {
|
||||
// Compute the prefix part for this field
|
||||
// Compute the prefix part for this field, normalized to end with _
|
||||
let prefix_part = leaf_name
|
||||
.strip_suffix(&suffix)
|
||||
.map(|s| {
|
||||
if s.is_empty() {
|
||||
String::new()
|
||||
} else if s.ends_with('_') {
|
||||
s.to_string()
|
||||
} else {
|
||||
format!("{}_", s)
|
||||
}
|
||||
})
|
||||
.map(normalize_prefix)
|
||||
.unwrap_or_default();
|
||||
field_parts.insert(field_name.clone(), prefix_part);
|
||||
}
|
||||
@@ -366,9 +367,18 @@ mod tests {
|
||||
fn test_get_pattern_instance_base_with_base_field() {
|
||||
// Simulates vbytes tree: has base field with block_vbytes leaf
|
||||
let tree = make_branch(vec![
|
||||
("base", make_branch(vec![("dateindex", make_leaf("block_vbytes"))])),
|
||||
("average", make_branch(vec![("dateindex", make_leaf("block_vbytes_average"))])),
|
||||
("sum", make_branch(vec![("dateindex", make_leaf("block_vbytes_sum"))])),
|
||||
(
|
||||
"base",
|
||||
make_branch(vec![("dateindex", make_leaf("block_vbytes"))]),
|
||||
),
|
||||
(
|
||||
"average",
|
||||
make_branch(vec![("dateindex", make_leaf("block_vbytes_average"))]),
|
||||
),
|
||||
(
|
||||
"sum",
|
||||
make_branch(vec![("dateindex", make_leaf("block_vbytes_sum"))]),
|
||||
),
|
||||
]);
|
||||
|
||||
let result = get_pattern_instance_base(&tree);
|
||||
@@ -380,11 +390,26 @@ mod tests {
|
||||
fn test_get_pattern_instance_base_without_base_field() {
|
||||
// Simulates weight tree: NO base field, only suffixed metrics
|
||||
let tree = make_branch(vec![
|
||||
("average", make_branch(vec![("dateindex", make_leaf("block_weight_average"))])),
|
||||
("sum", make_branch(vec![("dateindex", make_leaf("block_weight_sum"))])),
|
||||
("cumulative", make_branch(vec![("dateindex", make_leaf("block_weight_cumulative"))])),
|
||||
("max", make_branch(vec![("dateindex", make_leaf("block_weight_max"))])),
|
||||
("min", make_branch(vec![("dateindex", make_leaf("block_weight_min"))])),
|
||||
(
|
||||
"average",
|
||||
make_branch(vec![("dateindex", make_leaf("block_weight_average"))]),
|
||||
),
|
||||
(
|
||||
"sum",
|
||||
make_branch(vec![("dateindex", make_leaf("block_weight_sum"))]),
|
||||
),
|
||||
(
|
||||
"cumulative",
|
||||
make_branch(vec![("dateindex", make_leaf("block_weight_cumulative"))]),
|
||||
),
|
||||
(
|
||||
"max",
|
||||
make_branch(vec![("dateindex", make_leaf("block_weight_max"))]),
|
||||
),
|
||||
(
|
||||
"min",
|
||||
make_branch(vec![("dateindex", make_leaf("block_weight_min"))]),
|
||||
),
|
||||
]);
|
||||
|
||||
let result = get_pattern_instance_base(&tree);
|
||||
@@ -397,9 +422,18 @@ mod tests {
|
||||
// What if there's a "base" field that points to the same leaf as "average"?
|
||||
// This could happen if the tree generation creates a base field that shares leaves with average
|
||||
let tree = make_branch(vec![
|
||||
("base", make_branch(vec![("dateindex", make_leaf("block_weight_average"))])),
|
||||
("average", make_branch(vec![("dateindex", make_leaf("block_weight_average"))])),
|
||||
("sum", make_branch(vec![("dateindex", make_leaf("block_weight_sum"))])),
|
||||
(
|
||||
"base",
|
||||
make_branch(vec![("dateindex", make_leaf("block_weight_average"))]),
|
||||
),
|
||||
(
|
||||
"average",
|
||||
make_branch(vec![("dateindex", make_leaf("block_weight_average"))]),
|
||||
),
|
||||
(
|
||||
"sum",
|
||||
make_branch(vec![("dateindex", make_leaf("block_weight_sum"))]),
|
||||
),
|
||||
]);
|
||||
|
||||
let result = get_pattern_instance_base(&tree);
|
||||
|
||||
Reference in New Issue
Block a user