v: support -show-depgraph in addition to -show-callgraph

pull/10986/head
Delyan Angelov 2021-07-28 16:41:32 +03:00
parent e3cf95b058
commit d25bd95a0e
No known key found for this signature in database
GPG Key ID: 66886C0F12D595ED
6 changed files with 159 additions and 59 deletions

View File

@ -8,9 +8,10 @@ import v.ast
import v.vmod import v.vmod
import v.checker import v.checker
import v.parser import v.parser
import v.depgraph
import v.markused import v.markused
import v.depgraph
import v.callgraph import v.callgraph
import v.dotgraph
pub struct Builder { pub struct Builder {
pub: pub:
@ -52,6 +53,9 @@ pub fn new_builder(pref &pref.Preferences) Builder {
} }
} }
util.timing_set_should_print(pref.show_timings || pref.is_verbose) util.timing_set_should_print(pref.show_timings || pref.is_verbose)
if pref.show_callgraph || pref.show_depgraph {
dotgraph.start_digraph()
}
return Builder{ return Builder{
pref: pref pref: pref
table: table table: table
@ -180,6 +184,9 @@ pub fn (mut b Builder) resolve_deps() {
eprintln(deps_resolved.display()) eprintln(deps_resolved.display())
eprintln('------------------------------------------') eprintln('------------------------------------------')
} }
if b.pref.show_depgraph {
depgraph.show(deps_resolved, b.pref.path)
}
cycles := deps_resolved.display_cycles() cycles := deps_resolved.display_cycles()
if cycles.len > 1 { if cycles.len > 1 {
verror('error: import cycle detected between the following modules: \n' + cycles) verror('error: import cycle detected between the following modules: \n' + cycles)

View File

@ -3,7 +3,7 @@ module callgraph
import v.ast import v.ast
import v.ast.walker import v.ast.walker
import v.pref import v.pref
import strings import v.dotgraph
// callgraph.show walks the AST, starting at main() and prints a DOT output describing the calls // callgraph.show walks the AST, starting at main() and prints a DOT output describing the calls
// that function make transitively // that function make transitively
@ -11,18 +11,14 @@ pub fn show(mut table ast.Table, pref &pref.Preferences, ast_files []&ast.File)
mut mapper := &Mapper{ mut mapper := &Mapper{
pref: pref pref: pref
table: table table: table
dg: dotgraph.new('CallGraph', 'CallGraph for $pref.path', 'green')
} }
mapper.sb.writeln('digraph G {')
mapper.sb.writeln('\tedge [fontname="Helvetica",fontsize="10",labelfontname="Helvetica",labelfontsize="10",style="solid",color="black"];')
mapper.sb.writeln('\tnode [fontname="Helvetica",fontsize="10",style="filled",fontcolor="black",fillcolor="white",color="black",shape="box"];')
mapper.sb.writeln('\trankdir="LR";')
// Node14 [shape="box",label="PrivateBase",URL="$classPrivateBase.html"]; // Node14 [shape="box",label="PrivateBase",URL="$classPrivateBase.html"];
// Node15 -> Node9 [dir=back,color="midnightblue",fontsize=10,style="solid"]; // Node15 -> Node9 [dir=back,color="midnightblue",fontsize=10,style="solid"];
for afile in ast_files { for afile in ast_files {
walker.walk(mapper, afile) walker.walk(mapper, afile)
} }
mapper.sb.writeln('}') mapper.dg.finish()
println(mapper.sb.str())
} }
[heap] [heap]
@ -37,7 +33,7 @@ mut:
caller_name string caller_name string
dot_caller_name string dot_caller_name string
is_caller_used bool is_caller_used bool
sb strings.Builder = strings.new_builder(1024) dg dotgraph.DotGraph
} }
fn (mut m Mapper) dot_normalise_node_name(name string) string { fn (mut m Mapper) dot_normalise_node_name(name string) string {
@ -104,11 +100,10 @@ fn (mut m Mapper) visit(node &ast.Node) ? {
m.caller_name = m.fn_name(node.name, node.receiver.typ, node.is_method) m.caller_name = m.fn_name(node.name, node.receiver.typ, node.is_method)
m.dot_caller_name = m.dot_fn_name(node.name, node.receiver.typ, node.is_method) m.dot_caller_name = m.dot_fn_name(node.name, node.receiver.typ, node.is_method)
if m.is_caller_used { if m.is_caller_used {
if m.caller_name == 'main.main' { m.dg.new_node(m.caller_name,
m.sb.writeln('\t$m.dot_caller_name [label="fn main()",color="blue",height=0.2,width=0.4,fillcolor="#00FF00",tooltip="The main program entry point.",shape=oval];') node_name: m.dot_caller_name
} else { should_highlight: m.caller_name == 'main.main'
m.sb.writeln('\t$m.dot_caller_name [shape="box",label="$m.caller_name"];') )
}
} }
} }
else {} else {}
@ -121,11 +116,9 @@ fn (mut m Mapper) visit(node &ast.Node) ? {
dot_called_name := m.dot_fn_name(node.name, node.receiver_type, dot_called_name := m.dot_fn_name(node.name, node.receiver_type,
node.is_method) node.is_method)
// Node15 -> Node9 [dir=back,color="midnightblue",fontsize=10,style="solid"]; // Node15 -> Node9 [dir=back,color="midnightblue",fontsize=10,style="solid"];
if m.caller_name == 'main.main' { m.dg.new_edge(m.dot_caller_name, dot_called_name,
m.sb.writeln('\t$m.dot_caller_name -> $dot_called_name [color="blue"];') should_highlight: m.caller_name == 'main.main'
} else { )
m.sb.writeln('\t$m.dot_caller_name -> $dot_called_name')
}
} }
} }
else {} else {}
@ -134,13 +127,3 @@ fn (mut m Mapper) visit(node &ast.Node) ? {
else {} else {}
} }
} }
/*
mut fpath := ''
if m.file != 0 {
fpath = m.file.path
}
node_pos := node.position()
fpos := '$fpath:$node_pos.line_nr:$node_pos.col:'
println('$fpos $node.type_name() | $node')
*/

View File

@ -5,6 +5,8 @@
// this implementation is specifically suited to ordering dependencies // this implementation is specifically suited to ordering dependencies
module depgraph module depgraph
import v.dotgraph
struct DepGraphNode { struct DepGraphNode {
pub mut: pub mut:
name string name string
@ -197,3 +199,20 @@ fn (mut nn NodeNames) is_part_of_cycle(name string, already_seen []string) (bool
nn.is_cycle[name] = false nn.is_cycle[name] = false
return false, new_already_seen return false, new_already_seen
} }
pub fn show(graph &DepGraph, path string) {
mut dg := dotgraph.new('ModGraph', 'ModGraph for $path', 'blue')
mbuiltin := 'builtin'
for node in graph.nodes {
is_main := node.name == 'main'
dg.new_node(node.name, should_highlight: is_main)
mut deps := node.deps.clone()
if node.name != mbuiltin && mbuiltin !in deps {
deps << mbuiltin
}
for dep in deps {
dg.new_edge(node.name, dep, should_highlight: is_main)
}
}
dg.finish()
}

View File

@ -0,0 +1,8 @@
module dotgraph
pub fn start_digraph() {
println('digraph G {')
C.atexit(fn () {
println('}')
})
}

View File

@ -0,0 +1,81 @@
module dotgraph
import strings
[heap]
struct DotGraph {
mut:
sb strings.Builder
}
pub fn new(name string, label string, color string) &DotGraph {
mut res := &DotGraph{
sb: strings.new_builder(1024)
}
res.writeln(' subgraph cluster_$name {')
res.writeln('\tedge [fontname="Helvetica",fontsize="10",labelfontname="Helvetica",labelfontsize="10",style="solid",color="black"];')
res.writeln('\tnode [fontname="Helvetica",fontsize="10",style="filled",fontcolor="black",fillcolor="white",color="black",shape="box"];')
res.writeln('\trankdir="LR";')
res.writeln('\tcolor="$color";')
res.writeln('\tlabel="$label";')
// Node14 [shape="box",label="PrivateBase",URL="$classPrivateBase.html"];
// Node15 -> Node9 [dir=back,color="midnightblue",fontsize=10,style="solid"];
return res
}
pub fn (mut d DotGraph) writeln(line string) {
d.sb.writeln(line)
}
pub fn (mut d DotGraph) finish() {
d.sb.writeln(' }')
println(d.sb.str())
}
//
pub struct NewNodeConfig {
node_name string
should_highlight bool
tooltip string
ctx voidptr = voidptr(0)
name2node_fn FnLabel2NodeName = node_name
}
pub fn (mut d DotGraph) new_node(nlabel string, cfg NewNodeConfig) {
mut nname := cfg.name2node_fn(nlabel, cfg.ctx)
if cfg.node_name != '' {
nname = cfg.node_name
}
if cfg.should_highlight {
d.writeln('\t$nname [label="$nlabel",color="blue",height=0.2,width=0.4,fillcolor="#00FF00",tooltip="$cfg.tooltip",shape=oval];')
} else {
d.writeln('\t$nname [shape="box",label="$nlabel"];')
}
}
//
pub struct NewEdgeConfig {
should_highlight bool
ctx voidptr = voidptr(0)
name2node_fn FnLabel2NodeName = node_name
}
pub fn (mut d DotGraph) new_edge(source string, target string, cfg NewEdgeConfig) {
nsource := cfg.name2node_fn(source, cfg.ctx)
ntarget := cfg.name2node_fn(target, cfg.ctx)
if cfg.should_highlight {
d.writeln('\t$nsource -> $ntarget [color="blue"];')
} else {
d.writeln('\t$nsource -> $ntarget;')
}
}
//
pub type FnLabel2NodeName = fn (string, voidptr) string
pub fn node_name(name string, context voidptr) string {
return name.replace('.', '_')
}

View File

@ -98,37 +98,36 @@ pub mut:
// verbosity VerboseLevel // verbosity VerboseLevel
is_verbose bool is_verbose bool
// nofmt bool // disable vfmt // nofmt bool // disable vfmt
is_test bool // `v test string_test.v` is_test bool // `v test string_test.v`
is_script bool // single file mode (`v program.v`), main function can be skipped is_script bool // single file mode (`v program.v`), main function can be skipped
is_vsh bool // v script (`file.vsh`) file, the `os` module should be made global is_vsh bool // v script (`file.vsh`) file, the `os` module should be made global
is_livemain bool // main program that contains live/hot code is_livemain bool // main program that contains live/hot code
is_liveshared bool // a shared library, that will be used in a -live main program is_liveshared bool // a shared library, that will be used in a -live main program
is_shared bool // an ordinary shared library, -shared, no matter if it is live or not is_shared bool // an ordinary shared library, -shared, no matter if it is live or not
is_prof bool // benchmark every function is_prof bool // benchmark every function
profile_file string // the profile results will be stored inside profile_file profile_file string // the profile results will be stored inside profile_file
profile_no_inline bool // when true, [inline] functions would not be profiled profile_no_inline bool // when true, [inline] functions would not be profiled
translated bool // `v translate doom.v` are we running V code translated from C? allow globals, ++ expressions, etc translated bool // `v translate doom.v` are we running V code translated from C? allow globals, ++ expressions, etc
is_prod bool // use "-O2" is_prod bool // use "-O2"
obfuscate bool // `v -obf program.v`, renames functions to "f_XXX" obfuscate bool // `v -obf program.v`, renames functions to "f_XXX"
is_repl bool is_repl bool
is_run bool is_run bool
sanitize bool // use Clang's new "-fsanitize" option is_debug bool // turned on by -g or -cg, it tells v to pass -g to the C backend compiler.
is_debug bool // false by default, turned on by -g or -cg, it tells v to pass -g to the C backend compiler. is_vlines bool // turned on by -g (it slows down .tmp.c generation slightly).
sourcemap bool // JS Backend: -sourcemap will create a source map - default false
sourcemap_inline bool = true // JS Backend: -sourcemap-inline will embed the source map in the generated JaaScript file - currently default true only implemented
sourcemap_src_included bool // JS Backend: -sourcemap-src-included includes V source code in source map - default false
is_vlines bool // turned on by -g, false by default (it slows down .tmp.c generation slightly).
show_cc bool // -showcc, print cc command
show_c_output bool // -show-c-output, print all cc output even if the code was compiled correctly
show_callgraph bool // -show-callgraph, print the program callgraph, in a Graphviz DOT format to stdout
// NB: passing -cg instead of -g will set is_vlines to false and is_debug to true, thus making v generate cleaner C files, // NB: passing -cg instead of -g will set is_vlines to false and is_debug to true, thus making v generate cleaner C files,
// which are sometimes easier to debug / inspect manually than the .tmp.c files by plain -g (when/if v line number generation breaks). // which are sometimes easier to debug / inspect manually than the .tmp.c files by plain -g (when/if v line number generation breaks).
// use cached modules to speed up compilation. sanitize bool // use Clang's new "-fsanitize" option
dump_c_flags string // `-dump-c-flags file.txt` - let V store all C flags, passed to the backend C compiler sourcemap bool // JS Backend: -sourcemap will create a source map - default false
// in `file.txt`, one C flag/value per line. sourcemap_inline bool = true // JS Backend: -sourcemap-inline will embed the source map in the generated JaaScript file - currently default true only implemented
use_cache bool // = true sourcemap_src_included bool // JS Backend: -sourcemap-src-included includes V source code in source map - default false
retry_compilation bool = true // retry the compilation with another C compiler, if tcc fails. show_cc bool // -showcc, print cc command
is_stats bool // `v -stats file_test.v` will produce more detailed statistics for the tests that were run show_c_output bool // -show-c-output, print all cc output even if the code was compiled correctly
show_callgraph bool // -show-callgraph, print the program callgraph, in a Graphviz DOT format to stdout
show_depgraph bool // -show-depgraph, print the program module dependency graph, in a Graphviz DOT format to stdout
dump_c_flags string // `-dump-c-flags file.txt` - let V store all C flags, passed to the backend C compiler in `file.txt`, one C flag/value per line.
use_cache bool // when set, use cached modules to speed up subsequent compilations, at the cost of slower initial ones (while the modules are cached)
retry_compilation bool = true // retry the compilation with another C compiler, if tcc fails.
is_stats bool // `v -stats file_test.v` will produce more detailed statistics for the tests that were run
// TODO Convert this into a []string // TODO Convert this into a []string
cflags string // Additional options which will be passed to the C compiler. cflags string // Additional options which will be passed to the C compiler.
// For example, passing -cflags -Os will cause the C compiler to optimize the generated binaries for size. // For example, passing -cflags -Os will cause the C compiler to optimize the generated binaries for size.
@ -141,7 +140,7 @@ pub mut:
building_v bool building_v bool
autofree bool // `v -manualfree` => false, `v -autofree` => true; false by default for now. autofree bool // `v -manualfree` => false, `v -autofree` => true; false by default for now.
// Disabling `free()` insertion results in better performance in some applications (e.g. compilers) // Disabling `free()` insertion results in better performance in some applications (e.g. compilers)
compress bool compress bool // when set, use `upx` to compress the generated executable
// skip_builtin bool // Skips re-compilation of the builtin module // skip_builtin bool // Skips re-compilation of the builtin module
// to increase compilation time. // to increase compilation time.
// This is on by default, since a vast majority of users do not // This is on by default, since a vast majority of users do not
@ -444,6 +443,9 @@ pub fn parse_args(known_external_commands []string, args []string) (&Preferences
'-show-callgraph' { '-show-callgraph' {
res.show_callgraph = true res.show_callgraph = true
} }
'-show-depgraph' {
res.show_depgraph = true
}
'-dump-c-flags' { '-dump-c-flags' {
res.dump_c_flags = cmdline.option(current_args, arg, '-') res.dump_c_flags = cmdline.option(current_args, arg, '-')
i++ i++