global: snapshot

This commit is contained in:
nym21
2026-01-14 20:09:51 +01:00
parent d75c2a881b
commit 1c7434ff83
25 changed files with 4059 additions and 22606 deletions
+12
View File
@@ -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::*;
+7 -4
View File
@@ -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.
+53 -25
View File
@@ -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]
+77 -43
View File
@@ -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);