use core::fmt;
use std::borrow::Cow;
use itertools::Itertools;
use rspack_collections::{IdentifierMap, UkeyMap};
use rspack_util::env::has_query;
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
use crate::{
AsyncDependenciesBlockIdentifier, ChunkGroupUkey, ChunkUkey, Compilation, ModuleIdentifier,
};
pub mod chunk_graph_chunk;
pub mod chunk_graph_module;
pub use chunk_graph_chunk::{ChunkGraphChunk, ChunkSizeOptions};
pub use chunk_graph_module::{ChunkGraphModule, ModuleId};
#[derive(Debug, Clone, Default)]
pub struct ChunkGraph {
pub(crate) block_to_chunk_group_ukey: HashMap<AsyncDependenciesBlockIdentifier, ChunkGroupUkey>,
pub(crate) chunk_graph_module_by_module_identifier: IdentifierMap<ChunkGraphModule>,
chunk_graph_chunk_by_chunk_ukey: UkeyMap<ChunkUkey, ChunkGraphChunk>,
runtime_ids: HashMap<String, Option<String>>,
}
impl ChunkGraph {
pub fn is_entry_module(&self, module_id: &ModuleIdentifier) -> bool {
let cgm = self.expect_chunk_graph_module(*module_id);
!cgm.entry_in_chunks.is_empty()
}
}
static INDENT: &str = " ";
impl ChunkGraph {
pub fn to_dot(&self, compilation: &Compilation) -> std::result::Result<String, fmt::Error> {
let mut visited_group_nodes: HashMap<ChunkGroupUkey, String> = HashMap::default();
let mut visited_group_edges: HashSet<(ChunkGroupUkey, ChunkGroupUkey, bool)> =
HashSet::default();
let mut visiting_groups: Vec<ChunkGroupUkey> = Vec::new();
let module_graph = compilation.get_module_graph();
let get_debug_chunk_group_info = |chunk_group_ukey: &ChunkGroupUkey| {
let chunk_group = compilation
.chunk_group_by_ukey
.get(chunk_group_ukey)
.expect("should have chunk group");
let chunk_group_name = chunk_group.name().map_or_else(
|| {
let mut origins = chunk_group
.origins()
.iter()
.filter_map(|record| {
record.request.as_deref().and_then(|request| {
record.module.as_ref().map(|module_id| {
(
module_graph
.module_by_identifier(module_id)
.expect("should have module")
.readable_identifier(&compilation.options.context),
request,
)
})
})
})
.map(|(module, request)| format!("{module} {request}"))
.collect::<Vec<_>>();
origins.sort();
Cow::Owned(origins.join("\n"))
},
Cow::Borrowed,
);
let table_header = format!("<tr><td bgcolor=\"#aaa\">{chunk_group_name}</td></tr>");
let bg_color = if chunk_group.is_initial() {
"green"
} else {
"orange"
};
let requests = chunk_group
.chunks
.iter()
.map(|chunk_ukey| {
let chunk: &crate::Chunk = compilation
.chunk_by_ukey
.get(chunk_ukey)
.expect("should have chunk");
if let Some(name) = chunk.name() {
return name.to_string();
}
chunk_ukey.as_u32().to_string()
})
.map(|chunk_name| format!(" <tr><td>{chunk_name}</td></tr>"))
.join("\n");
let table_body = requests.clone();
format!(
r#"
<<table bgcolor="{bg_color}">
{table_header}
{table_body}
</table>>
"#
)
};
for (_, chunk_group_ukey) in compilation.entrypoints() {
visiting_groups.push(*chunk_group_ukey);
}
while let Some(chunk_group_ukey) = visiting_groups.pop() {
let chunk_group = compilation
.chunk_group_by_ukey
.get(&chunk_group_ukey)
.expect("should have chunk group");
if visited_group_nodes.contains_key(&chunk_group_ukey) {
continue;
}
let chunk_group_name = get_debug_chunk_group_info(&chunk_group_ukey);
for parent in &chunk_group.parents {
visited_group_edges.insert((chunk_group_ukey, *parent, false));
}
for child in chunk_group.children.iter() {
visited_group_edges.insert((chunk_group_ukey, *child, true));
visiting_groups.push(*child);
}
visited_group_nodes.insert(chunk_group_ukey, chunk_group_name.clone());
}
use std::fmt::Write;
let mut dot = String::new();
writeln!(&mut dot, "digraph G {{")?;
writeln!(&mut dot, "layout=neato;")?;
writeln!(&mut dot, "overlap=false;")?;
writeln!(&mut dot, "node [shape=plaintext];")?;
writeln!(&mut dot, "edge [arrowsize=0.5];")?;
for (node_id, node_info) in visited_group_nodes.iter() {
writeln!(&mut dot, "{} {} [", INDENT, node_id.as_u32())?;
write!(&mut dot, "label={node_info}")?;
write!(&mut dot, "\n];\n")?;
}
for edge in visited_group_edges.iter() {
write!(&mut dot, "{} -> {}", edge.0.as_u32(), edge.1.as_u32())?;
write!(&mut dot, "[")?;
write!(
&mut dot,
"style=\"{}\"",
if edge.2 { "solid" } else { "dotted" }
)?;
write!(&mut dot, "]")?;
writeln!(&mut dot, ";")?;
}
write!(&mut dot, "}}")?;
Ok(dot)
}
pub async fn generate_dot(&self, compilation: &Compilation, dotfile_name: &str) {
if !has_query() {
return;
}
let result = self.to_dot(compilation).expect("to_dot failed");
compilation
.output_filesystem
.write(
format!(
"{}-{}.dot",
compilation.compiler_id().as_u32(),
dotfile_name
)
.as_str()
.into(),
result.as_bytes(),
)
.await
.expect("write dot file failed");
}
}