use std::cell::RefCell;
use std::rc::Rc;
use graphrefly_core::CoreFull;
use serde::Serialize;
use crate::describe::{describe_of, NodeStatus, NodeTypeStr};
use crate::graph::{Graph, GraphInner};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum OrphanKind {
OrphanEffect,
IdleDerived,
IdleProducer,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NodeProfile {
pub path: String,
#[serde(rename = "type")]
pub r#type: String,
pub status: String,
pub subscriber_count: usize,
pub dep_count: usize,
pub is_orphan_effect: bool,
pub orphan_kind: Option<OrphanKind>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GraphProfileResult {
pub node_count: usize,
pub edge_count: usize,
pub subgraph_count: usize,
pub nodes: Vec<NodeProfile>,
pub hotspots: Hotspots,
pub orphans: Vec<NodeProfile>,
pub orphan_effects: Vec<NodeProfile>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Hotspots {
pub by_subscriber_count: Vec<NodeProfile>,
pub by_dep_count: Vec<NodeProfile>,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct GraphProfileOptions {
pub top_n: Option<usize>,
}
impl Graph {
#[must_use]
pub fn resource_profile(
&self,
core: &dyn CoreFull,
opts: Option<GraphProfileOptions>,
) -> GraphProfileResult {
resource_profile_of(core, &self.inner, opts)
}
}
fn resource_profile_of(
core: &dyn CoreFull,
inner_arc: &Rc<RefCell<GraphInner>>,
opts: Option<GraphProfileOptions>,
) -> GraphProfileResult {
let top_n = opts.and_then(|o| o.top_n).unwrap_or(10);
let desc = describe_of(core, inner_arc, None);
let names_to_id: Vec<(String, graphrefly_core::NodeId)> = {
let inner = inner_arc.borrow_mut();
inner.names.iter().map(|(n, id)| (n.clone(), *id)).collect()
};
let mut profiles: Vec<NodeProfile> = Vec::with_capacity(names_to_id.len());
for (path, node_id) in &names_to_id {
let Some(node_desc) = desc.nodes.get(path) else {
continue;
};
let type_str = node_type_str(node_desc.r#type);
let status_str = node_status_str(node_desc.status);
let subscriber_count = core.sink_count_of(*node_id);
let dep_count = node_desc.deps.len();
let is_orphan_effect = type_str == "effect" && subscriber_count == 0;
let orphan_kind = if subscriber_count == 0 {
match type_str {
"effect" => Some(OrphanKind::OrphanEffect),
"derived" | "dynamic" => Some(OrphanKind::IdleDerived),
"producer" => Some(OrphanKind::IdleProducer),
_ => None,
}
} else {
None
};
profiles.push(NodeProfile {
path: path.clone(),
r#type: type_str.to_string(),
status: status_str.to_string(),
subscriber_count,
dep_count,
is_orphan_effect,
orphan_kind,
});
}
let top_by = |key: fn(&NodeProfile) -> usize| -> Vec<NodeProfile> {
let mut sorted: Vec<NodeProfile> = profiles.clone();
sorted.sort_by_key(|n| std::cmp::Reverse(key(n)));
sorted.truncate(top_n);
sorted
};
let orphans: Vec<NodeProfile> = profiles
.iter()
.filter(|p| p.orphan_kind.is_some())
.cloned()
.collect();
let orphan_effects: Vec<NodeProfile> = profiles
.iter()
.filter(|p| p.is_orphan_effect)
.cloned()
.collect();
GraphProfileResult {
node_count: profiles.len(),
edge_count: desc.edges.len(),
subgraph_count: desc.subgraphs.len(),
hotspots: Hotspots {
by_subscriber_count: top_by(|p| p.subscriber_count),
by_dep_count: top_by(|p| p.dep_count),
},
orphans,
orphan_effects,
nodes: profiles,
}
}
fn node_type_str(t: NodeTypeStr) -> &'static str {
match t {
NodeTypeStr::State => "state",
NodeTypeStr::Derived => "derived",
NodeTypeStr::Dynamic => "dynamic",
NodeTypeStr::Producer => "producer",
NodeTypeStr::Effect => "effect",
NodeTypeStr::Operator => "operator",
}
}
fn node_status_str(s: NodeStatus) -> &'static str {
match s {
NodeStatus::Sentinel => "sentinel",
NodeStatus::Pending => "pending",
NodeStatus::Dirty => "dirty",
NodeStatus::Settled => "settled",
NodeStatus::Resolved => "resolved",
NodeStatus::Completed => "completed",
NodeStatus::Errored => "errored",
}
}