├── call_graph.png ├── spec ├── call_graph.png └── integration_spec.rb ├── bin └── console ├── Gemfile ├── Gemfile.lock ├── lib ├── visual_call_graph.rb └── visual_call_graph │ ├── tracer.rb │ └── graph_manager.rb ├── README.md └── visual_call_graph.gemspec /call_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matugm/visual-call-graph/HEAD/call_graph.png -------------------------------------------------------------------------------- /spec/call_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matugm/visual-call-graph/HEAD/spec/call_graph.png -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "visual_call_graph" 5 | 6 | require "irb" 7 | IRB.start(__FILE__) 8 | 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 6 | 7 | gemspec 8 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | visual_call_graph (0.5.0) 5 | ruby-graphviz (~> 1.2.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | ruby-graphviz (1.2.3) 11 | 12 | PLATFORMS 13 | ruby 14 | 15 | DEPENDENCIES 16 | visual_call_graph! 17 | 18 | BUNDLED WITH 19 | 1.16.1 20 | -------------------------------------------------------------------------------- /lib/visual_call_graph.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'visual_call_graph/graph_manager' 3 | require 'visual_call_graph/tracer' 4 | 5 | module VisualCallGraph 6 | extend self 7 | 8 | def trace(options = {}) 9 | unless block_given? 10 | puts "Block required!" 11 | return 12 | end 13 | 14 | tracer = Tracer.new(options) 15 | 16 | tracer.enable 17 | yield 18 | tracer.disable 19 | 20 | tracer.generate_output_png 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Visual Call Graph 3 | 4 | This gem will produce a call graph using Graphviz. 5 | 6 | ![https://i2.wp.com/www.rubyguides.com/wp-content/uploads/2017/01/visual_call_graph.png](https://i2.wp.com/www.rubyguides.com/wp-content/uploads/2017/01/visual_call_graph.png) 7 | 8 | ## Example 9 | 10 | ```ruby 11 | require 'visual_call_graph' 12 | 13 | VisualCallGraph.trace { Users.all } 14 | ``` 15 | 16 | ## More Details 17 | 18 | [https://www.rubyguides.com/2017/01/spy-on-your-ruby-methods/](https://www.rubyguides.com/2017/01/spy-on-your-ruby-methods/) 19 | 20 | -------------------------------------------------------------------------------- /spec/integration_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../lib/visual_call_graph' 2 | 3 | describe VisualCallGraph do 4 | it "returns the correct amount of nodes" do 5 | expect { VisualCallGraph.trace { Foo.aaa } } 6 | .to output("Call graph created with a total of 4 nodes.\n") 7 | .to_stdout 8 | end 9 | 10 | it "creates an output file" do 11 | expect(File.exists?("call_graph.png")).to be_truthy 12 | end 13 | end 14 | 15 | module Foo 16 | extend self 17 | 18 | def aaa 19 | bbb 20 | ccc 21 | end 22 | 23 | def bbb 24 | ccc 25 | end 26 | 27 | def ccc 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /visual_call_graph.gemspec: -------------------------------------------------------------------------------- 1 | 2 | Gem::Specification.new do |s| 3 | s.name = 'visual_call_graph' 4 | s.version = '0.5.0' 5 | s.summary = "This gem helps you see all the other methods called by another method." 6 | s.description = "Uses the TracePoint class & Graphviz to generate a visual representation of all the methods called by another method." 7 | s.authors = ["Jesus Castello"] 8 | s.email = 'jesus@rubyguides.com' 9 | s.homepage = 'https://www.rubyguides.com' 10 | s.files = Dir["lib/*.rb"] 11 | s.license = 'MIT' 12 | 13 | s.require_paths = ["lib"] 14 | 15 | s.add_runtime_dependency 'ruby-graphviz', '~> 1.2.0' 16 | end 17 | 18 | -------------------------------------------------------------------------------- /lib/visual_call_graph/tracer.rb: -------------------------------------------------------------------------------- 1 | 2 | class Tracer 3 | def initialize(options) 4 | @graph = GraphManager.new(options) 5 | @tracer = build_tracer 6 | end 7 | 8 | def enable 9 | @tracer.enable 10 | end 11 | 12 | def disable 13 | @tracer.disable 14 | end 15 | 16 | def generate_output_png 17 | @graph.output 18 | 19 | puts "Call graph created with a total of #{node_count}." 20 | end 21 | 22 | private 23 | 24 | def build_tracer 25 | TracePoint.new(:call, :return) { |event| 26 | next if event.defined_class == self.class 27 | 28 | case event.event 29 | when :return 30 | @graph.pop 31 | when :call 32 | @graph.add_edges(event) 33 | end 34 | } 35 | end 36 | 37 | def node_count 38 | "#{@graph.node_count} #{(@graph.node_count > 1 ? 'nodes' : 'node')}" 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/visual_call_graph/graph_manager.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'graphviz' 3 | 4 | class GraphManager 5 | def initialize(options) 6 | @stack = ["start"] 7 | @edges = [] 8 | @options = options 9 | 10 | @g = GraphViz.new(:G, :type => :digraph) 11 | 12 | @g.add_node("start") 13 | end 14 | 15 | def add_edges(event) 16 | node = get_node_name(event) 17 | edge = [@stack.last, node] 18 | 19 | @stack << node 20 | 21 | return if @edges.include?(edge) 22 | 23 | @edges << edge 24 | @g.add_edge(*@edges.last) 25 | end 26 | 27 | def get_node_name(event) 28 | if @options[:show_path] 29 | "#{event.defined_class}##{event.method_id}\n#{event.path}".freeze 30 | else 31 | "#{event.defined_class}##{event.method_id}".freeze 32 | end 33 | end 34 | 35 | def output 36 | format = @options[:format] || :png 37 | path = @options[:path] || "#{Dir.pwd}/call_graph.#{format.to_s}" 38 | 39 | @g.output(format.to_sym => path) 40 | end 41 | 42 | def node_count 43 | @g.node_count 44 | end 45 | 46 | def pop 47 | @stack.pop 48 | end 49 | end 50 | --------------------------------------------------------------------------------