mirror of
https://github.com/ruby/ruby.git
synced 2025-08-15 13:39:04 +02:00
ZJIT: Add a graphviz dumper for HIR (#14117)
This is moderately useful just in stdout (copy and paste into a renderer) but potentially more useful alongside a tool that parses stdout looking for `digraph G { ... }` and renders those automatically.
This commit is contained in:
parent
057d7c1c58
commit
8eb26ebf91
2 changed files with 180 additions and 0 deletions
176
zjit/src/hir.rs
176
zjit/src/hir.rs
|
@ -843,6 +843,22 @@ impl<'a> FunctionPrinter<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pretty printer for [`Function`].
|
||||||
|
pub struct FunctionGraphvizPrinter<'a> {
|
||||||
|
fun: &'a Function,
|
||||||
|
ptr_map: PtrPrintMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> FunctionGraphvizPrinter<'a> {
|
||||||
|
pub fn new(fun: &'a Function) -> Self {
|
||||||
|
let mut ptr_map = PtrPrintMap::identity();
|
||||||
|
if cfg!(test) {
|
||||||
|
ptr_map.map_ptrs = true;
|
||||||
|
}
|
||||||
|
Self { fun, ptr_map }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Union-Find (Disjoint-Set) is a data structure for managing disjoint sets that has an interface
|
/// Union-Find (Disjoint-Set) is a data structure for managing disjoint sets that has an interface
|
||||||
/// of two operations:
|
/// of two operations:
|
||||||
///
|
///
|
||||||
|
@ -2115,6 +2131,10 @@ impl Function {
|
||||||
Some(DumpHIR::Debug) => println!("Optimized HIR:\n{:#?}", &self),
|
Some(DumpHIR::Debug) => println!("Optimized HIR:\n{:#?}", &self),
|
||||||
None => {},
|
None => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if get_option!(dump_hir_graphviz) {
|
||||||
|
println!("{}", FunctionGraphvizPrinter::new(&self));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -2293,6 +2313,87 @@ impl<'a> std::fmt::Display for FunctionPrinter<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct HtmlEncoder<'a, 'b> {
|
||||||
|
formatter: &'a mut std::fmt::Formatter<'b>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> std::fmt::Write for HtmlEncoder<'a, 'b> {
|
||||||
|
fn write_str(&mut self, s: &str) -> std::fmt::Result {
|
||||||
|
for ch in s.chars() {
|
||||||
|
match ch {
|
||||||
|
'<' => self.formatter.write_str("<")?,
|
||||||
|
'>' => self.formatter.write_str(">")?,
|
||||||
|
'&' => self.formatter.write_str("&")?,
|
||||||
|
'"' => self.formatter.write_str(""")?,
|
||||||
|
'\'' => self.formatter.write_str("'")?,
|
||||||
|
_ => self.formatter.write_char(ch)?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> std::fmt::Display for FunctionGraphvizPrinter<'a> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
macro_rules! write_encoded {
|
||||||
|
($f:ident, $($arg:tt)*) => {
|
||||||
|
HtmlEncoder { formatter: $f }.write_fmt(format_args!($($arg)*))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
use std::fmt::Write;
|
||||||
|
let fun = &self.fun;
|
||||||
|
let iseq_name = iseq_get_location(fun.iseq, 0);
|
||||||
|
write!(f, "digraph G {{ # ")?;
|
||||||
|
write_encoded!(f, "{iseq_name}")?;
|
||||||
|
write!(f, "\n")?;
|
||||||
|
writeln!(f, "node [shape=plaintext];")?;
|
||||||
|
writeln!(f, "mode=hier; overlap=false; splines=true;")?;
|
||||||
|
for block_id in fun.rpo() {
|
||||||
|
writeln!(f, r#" {block_id} [label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">"#)?;
|
||||||
|
write!(f, r#"<TR><TD ALIGN="LEFT" PORT="params" BGCOLOR="gray">{block_id}("#)?;
|
||||||
|
if !fun.blocks[block_id.0].params.is_empty() {
|
||||||
|
let mut sep = "";
|
||||||
|
for param in &fun.blocks[block_id.0].params {
|
||||||
|
write_encoded!(f, "{sep}{param}")?;
|
||||||
|
let insn_type = fun.type_of(*param);
|
||||||
|
if !insn_type.is_subtype(types::Empty) {
|
||||||
|
write_encoded!(f, ":{}", insn_type.print(&self.ptr_map))?;
|
||||||
|
}
|
||||||
|
sep = ", ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut edges = vec![];
|
||||||
|
writeln!(f, ") </TD></TR>")?;
|
||||||
|
for insn_id in &fun.blocks[block_id.0].insns {
|
||||||
|
let insn_id = fun.union_find.borrow().find_const(*insn_id);
|
||||||
|
let insn = fun.find(insn_id);
|
||||||
|
if matches!(insn, Insn::Snapshot {..}) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
write!(f, r#"<TR><TD ALIGN="left" PORT="{insn_id}">"#)?;
|
||||||
|
if insn.has_output() {
|
||||||
|
let insn_type = fun.type_of(insn_id);
|
||||||
|
if insn_type.is_subtype(types::Empty) {
|
||||||
|
write_encoded!(f, "{insn_id} = ")?;
|
||||||
|
} else {
|
||||||
|
write_encoded!(f, "{insn_id}:{} = ", insn_type.print(&self.ptr_map))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Insn::Jump(ref target) | Insn::IfTrue { ref target, .. } | Insn::IfFalse { ref target, .. } = insn {
|
||||||
|
edges.push((insn_id, target.target));
|
||||||
|
}
|
||||||
|
write_encoded!(f, "{}", insn.print(&self.ptr_map))?;
|
||||||
|
writeln!(f, " </TD></TR>")?;
|
||||||
|
}
|
||||||
|
writeln!(f, "</TABLE>>];")?;
|
||||||
|
for (src, dst) in edges {
|
||||||
|
writeln!(f, " {block_id}:{src} -> {dst}:params;")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeln!(f, "}}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct FrameState {
|
pub struct FrameState {
|
||||||
iseq: IseqPtr,
|
iseq: IseqPtr,
|
||||||
|
@ -5145,6 +5246,81 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod graphviz_tests {
|
||||||
|
use super::*;
|
||||||
|
use expect_test::{expect, Expect};
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn assert_optimized_graphviz(method: &str, expected: Expect) {
|
||||||
|
let iseq = crate::cruby::with_rubyvm(|| get_method_iseq("self", method));
|
||||||
|
unsafe { crate::cruby::rb_zjit_profile_disable(iseq) };
|
||||||
|
let mut function = iseq_to_hir(iseq).unwrap();
|
||||||
|
function.optimize();
|
||||||
|
function.validate().unwrap();
|
||||||
|
let actual = format!("{}", FunctionGraphvizPrinter::new(&function));
|
||||||
|
expected.assert_eq(&actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_guard_fixnum_or_fixnum() {
|
||||||
|
eval(r#"
|
||||||
|
def test(x, y) = x | y
|
||||||
|
|
||||||
|
test(1, 2)
|
||||||
|
"#);
|
||||||
|
assert_optimized_graphviz("test", expect![[r#"
|
||||||
|
digraph G { # test@<compiled>:2
|
||||||
|
node [shape=plaintext];
|
||||||
|
mode=hier; overlap=false; splines=true;
|
||||||
|
bb0 [label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
|
||||||
|
<TR><TD ALIGN="LEFT" PORT="params" BGCOLOR="gray">bb0(v0:BasicObject, v1:BasicObject, v2:BasicObject) </TD></TR>
|
||||||
|
<TR><TD ALIGN="left" PORT="v7">PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, 29) </TD></TR>
|
||||||
|
<TR><TD ALIGN="left" PORT="v8">v8:Fixnum = GuardType v1, Fixnum </TD></TR>
|
||||||
|
<TR><TD ALIGN="left" PORT="v9">v9:Fixnum = GuardType v2, Fixnum </TD></TR>
|
||||||
|
<TR><TD ALIGN="left" PORT="v10">v10:Fixnum = FixnumOr v8, v9 </TD></TR>
|
||||||
|
<TR><TD ALIGN="left" PORT="v6">Return v10 </TD></TR>
|
||||||
|
</TABLE>>];
|
||||||
|
}
|
||||||
|
"#]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_multiple_blocks() {
|
||||||
|
eval(r#"
|
||||||
|
def test(c)
|
||||||
|
if c
|
||||||
|
3
|
||||||
|
else
|
||||||
|
4
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test(1)
|
||||||
|
test("x")
|
||||||
|
"#);
|
||||||
|
assert_optimized_graphviz("test", expect![[r#"
|
||||||
|
digraph G { # test@<compiled>:3
|
||||||
|
node [shape=plaintext];
|
||||||
|
mode=hier; overlap=false; splines=true;
|
||||||
|
bb0 [label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
|
||||||
|
<TR><TD ALIGN="LEFT" PORT="params" BGCOLOR="gray">bb0(v0:BasicObject, v1:BasicObject) </TD></TR>
|
||||||
|
<TR><TD ALIGN="left" PORT="v3">v3:CBool = Test v1 </TD></TR>
|
||||||
|
<TR><TD ALIGN="left" PORT="v4">IfFalse v3, bb1(v0, v1) </TD></TR>
|
||||||
|
<TR><TD ALIGN="left" PORT="v5">v5:Fixnum[3] = Const Value(3) </TD></TR>
|
||||||
|
<TR><TD ALIGN="left" PORT="v6">Return v5 </TD></TR>
|
||||||
|
</TABLE>>];
|
||||||
|
bb0:v4 -> bb1:params;
|
||||||
|
bb1 [label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
|
||||||
|
<TR><TD ALIGN="LEFT" PORT="params" BGCOLOR="gray">bb1(v7:BasicObject, v8:BasicObject) </TD></TR>
|
||||||
|
<TR><TD ALIGN="left" PORT="v10">v10:Fixnum[4] = Const Value(4) </TD></TR>
|
||||||
|
<TR><TD ALIGN="left" PORT="v11">Return v10 </TD></TR>
|
||||||
|
</TABLE>>];
|
||||||
|
}
|
||||||
|
"#]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod opt_tests {
|
mod opt_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -37,6 +37,8 @@ pub struct Options {
|
||||||
/// Dump High-level IR after optimization, right before codegen.
|
/// Dump High-level IR after optimization, right before codegen.
|
||||||
pub dump_hir_opt: Option<DumpHIR>,
|
pub dump_hir_opt: Option<DumpHIR>,
|
||||||
|
|
||||||
|
pub dump_hir_graphviz: bool,
|
||||||
|
|
||||||
/// Dump low-level IR
|
/// Dump low-level IR
|
||||||
pub dump_lir: bool,
|
pub dump_lir: bool,
|
||||||
|
|
||||||
|
@ -61,6 +63,7 @@ impl Default for Options {
|
||||||
debug: false,
|
debug: false,
|
||||||
dump_hir_init: None,
|
dump_hir_init: None,
|
||||||
dump_hir_opt: None,
|
dump_hir_opt: None,
|
||||||
|
dump_hir_graphviz: false,
|
||||||
dump_lir: false,
|
dump_lir: false,
|
||||||
dump_disasm: false,
|
dump_disasm: false,
|
||||||
perf: false,
|
perf: false,
|
||||||
|
@ -186,6 +189,7 @@ fn parse_option(str_ptr: *const std::os::raw::c_char) -> Option<()> {
|
||||||
("dump-hir" | "dump-hir-opt", "") => options.dump_hir_opt = Some(DumpHIR::WithoutSnapshot),
|
("dump-hir" | "dump-hir-opt", "") => options.dump_hir_opt = Some(DumpHIR::WithoutSnapshot),
|
||||||
("dump-hir" | "dump-hir-opt", "all") => options.dump_hir_opt = Some(DumpHIR::All),
|
("dump-hir" | "dump-hir-opt", "all") => options.dump_hir_opt = Some(DumpHIR::All),
|
||||||
("dump-hir" | "dump-hir-opt", "debug") => options.dump_hir_opt = Some(DumpHIR::Debug),
|
("dump-hir" | "dump-hir-opt", "debug") => options.dump_hir_opt = Some(DumpHIR::Debug),
|
||||||
|
("dump-hir-graphviz", "") => options.dump_hir_graphviz = true,
|
||||||
|
|
||||||
("dump-hir-init", "") => options.dump_hir_init = Some(DumpHIR::WithoutSnapshot),
|
("dump-hir-init", "") => options.dump_hir_init = Some(DumpHIR::WithoutSnapshot),
|
||||||
("dump-hir-init", "all") => options.dump_hir_init = Some(DumpHIR::All),
|
("dump-hir-init", "all") => options.dump_hir_init = Some(DumpHIR::All),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue