kefka 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,6 +1,14 @@
1
1
  source "http://rubygems.org"
2
2
 
3
+ gem "yajl-ruby"
4
+ gem "coderay"
5
+ gem "sinatra"
6
+ gem "method_source"
7
+ gem "rgl"
8
+
3
9
  group :development do
10
+ gem "pry"
11
+ gem "pry-doc"
4
12
  gem "rspec"
5
13
  gem "jeweler"
6
14
  end
data/README CHANGED
@@ -1,6 +1,6 @@
1
- == Kefka
1
+ == Kefka (Experimental)
2
2
 
3
- A tool for understanding unfamiliar codebases and 3rd party libraries. It shows you callgraphs for different execution paths of a program.
3
+ A tool for understanding unfamiliar codebases and 3rd party libraries. Basic Idea is to visualize the method callgraph of a program while showing local variable values for each line of execution.
4
4
 
5
5
  == Installation
6
6
 
@@ -8,9 +8,8 @@ A tool for understanding unfamiliar codebases and 3rd party libraries. It shows
8
8
 
9
9
  == Usage
10
10
 
11
- From the command line ,
12
-
13
- `kefka filename.rb`
11
+ 1. From the command line , `kefka`
12
+ 2. Go to browser and point to http://localhost:4567/
14
13
 
15
14
  Examples are provided under the examples directory of kefka gem
16
15
 
data/TODO CHANGED
@@ -1,5 +1,9 @@
1
1
  == TODO
2
2
 
3
+ http://wishlabs.blogspot.ca/2011/09/interactive-dot-generated-svg-graph.html
4
+ http://www.ibm.com/developerworks/library/l-graphvis/
5
+ http://bl.ocks.org/1667139
6
+
3
7
  1st Iteration
4
8
 
5
9
  method graph
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.1
1
+ 0.0.2
data/app.rb ADDED
@@ -0,0 +1,36 @@
1
+ $LOAD_PATH.unshift("#{File.expand_path(File.dirname(__FILE__))}/lib")
2
+
3
+ require 'sinatra'
4
+ require 'kefka'
5
+ require 'yajl'
6
+ require 'yajl/json_gem'
7
+
8
+ get '/' do
9
+ erb :index
10
+ end
11
+
12
+ get '/callgraph' do
13
+ content_type :json
14
+
15
+ path = "examples/sample_a.rb"
16
+ file = File.open(path)
17
+
18
+ @tracer = Kefka::Tracer.new
19
+ @tracer.trace(file, :callgraph_handler)
20
+
21
+ graph = @tracer.method_graph
22
+ graph.vertices.each { |method| method.format = :html }
23
+ graph.to_json
24
+ end
25
+
26
+ get '/locals' do
27
+ content_type :json
28
+
29
+ path = "examples/sample_a.rb"
30
+ file = File.open(path)
31
+
32
+ tracer = Kefka::Tracer.new
33
+ tracer.trace(file, :local_values_handler)
34
+ results = tracer.local_values
35
+ results.to_json
36
+ end
data/bin/kefka CHANGED
@@ -3,16 +3,10 @@
3
3
  $LOAD_PATH.unshift("#{File.expand_path(File.dirname(__FILE__))}/../lib")
4
4
 
5
5
  require 'kefka'
6
- require 'pp'
7
6
 
8
- if ARGV.count < 1
9
- puts "Usage: kefka [path_to_file]"
10
- exit
11
- end
7
+ RUBY = File.join(*RbConfig::CONFIG.values_at("bindir", "ruby_install_name")) + RbConfig::CONFIG["EXEEXT"]
12
8
 
13
- path = ARGV[0]
14
- file = File.open(path)
9
+ APP = "#{File.expand_path(File.dirname(__FILE__))}/../app.rb"
10
+ exec RUBY, APP
15
11
 
16
- Kefka.trace(file, :callgraph_handler)
17
- Kefka.display
18
12
 
@@ -0,0 +1,61 @@
1
+ require 'forwardable'
2
+ require 'date'
3
+ require 'net/http'
4
+
5
+ class Book
6
+ extend Forwardable
7
+ def_delegators :@store, :first, :last
8
+
9
+ def initialize
10
+ @store = [1,2]
11
+ end
12
+
13
+ def []
14
+ "array accessor"
15
+ end
16
+ end
17
+
18
+ class Displayer
19
+ def initialize(out)
20
+ @out = out
21
+ end
22
+
23
+ def puts(val)
24
+ @out.puts val
25
+ end
26
+ end
27
+
28
+ def hello
29
+ num = init
30
+ x = 4
31
+ x = x * 2
32
+ b = Book.new
33
+
34
+ if b.first
35
+ puts "lol"
36
+ end
37
+
38
+ c = b[]
39
+ display = Displayer.new($stdout)
40
+ display.puts "Hello #{num}"
41
+ end
42
+
43
+ def init
44
+ a = 1 + rand(9)
45
+ (0..3).each do |x|
46
+ a = a + time_diff
47
+ end
48
+ a
49
+ end
50
+
51
+ # function called inside a loop
52
+ def time_diff
53
+ before = Time.now
54
+ after = Time.now
55
+ (after - before).to_i
56
+ end
57
+
58
+ hello
59
+
60
+ Net::HTTP.get_response(URI.parse("http://www.twitter.com")).body
61
+
data/kefka.gemspec CHANGED
@@ -5,18 +5,17 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "kefka"
8
- s.version = "0.0.1"
8
+ s.version = "0.0.2"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Reginald Tan"]
12
- s.date = "2012-07-14"
12
+ s.date = "2012-07-30"
13
13
  s.description = " It traces the execution path of a program and displays the source code of each method call in the callgraph "
14
14
  s.email = "redge.tan@gmail.com"
15
15
  s.executables = ["kefka"]
16
16
  s.extra_rdoc_files = [
17
17
  "LICENSE.txt",
18
18
  "README",
19
- "README.rdoc",
20
19
  "TODO"
21
20
  ]
22
21
  s.files = [
@@ -24,16 +23,20 @@ Gem::Specification.new do |s|
24
23
  "Gemfile",
25
24
  "LICENSE.txt",
26
25
  "README",
27
- "README.rdoc",
28
26
  "Rakefile",
29
27
  "VERSION",
28
+ "app.rb",
30
29
  "bin/kefka",
31
- "examples/sample1.rb",
30
+ "examples/sample_a.rb",
32
31
  "examples/trace_specific_lines.rb",
33
32
  "kefka.gemspec",
34
33
  "lib/kefka.rb",
34
+ "public/javascripts/app.js",
35
+ "public/javascripts/jquery-1.7.2.js",
36
+ "public/stylesheets/application.css",
35
37
  "test/helper.rb",
36
- "test/test_kefka.rb"
38
+ "test/test_kefka.rb",
39
+ "views/index.erb"
37
40
  ]
38
41
  s.homepage = "http://github.com/redgetan/kefka"
39
42
  s.licenses = ["MIT"]
@@ -45,13 +48,34 @@ Gem::Specification.new do |s|
45
48
  s.specification_version = 3
46
49
 
47
50
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
51
+ s.add_runtime_dependency(%q<yajl-ruby>, [">= 0"])
52
+ s.add_runtime_dependency(%q<coderay>, [">= 0"])
53
+ s.add_runtime_dependency(%q<sinatra>, [">= 0"])
54
+ s.add_runtime_dependency(%q<method_source>, [">= 0"])
55
+ s.add_runtime_dependency(%q<rgl>, [">= 0"])
56
+ s.add_development_dependency(%q<pry>, [">= 0"])
57
+ s.add_development_dependency(%q<pry-doc>, [">= 0"])
48
58
  s.add_development_dependency(%q<rspec>, [">= 0"])
49
59
  s.add_development_dependency(%q<jeweler>, [">= 0"])
50
60
  else
61
+ s.add_dependency(%q<yajl-ruby>, [">= 0"])
62
+ s.add_dependency(%q<coderay>, [">= 0"])
63
+ s.add_dependency(%q<sinatra>, [">= 0"])
64
+ s.add_dependency(%q<method_source>, [">= 0"])
65
+ s.add_dependency(%q<rgl>, [">= 0"])
66
+ s.add_dependency(%q<pry>, [">= 0"])
67
+ s.add_dependency(%q<pry-doc>, [">= 0"])
51
68
  s.add_dependency(%q<rspec>, [">= 0"])
52
69
  s.add_dependency(%q<jeweler>, [">= 0"])
53
70
  end
54
71
  else
72
+ s.add_dependency(%q<yajl-ruby>, [">= 0"])
73
+ s.add_dependency(%q<coderay>, [">= 0"])
74
+ s.add_dependency(%q<sinatra>, [">= 0"])
75
+ s.add_dependency(%q<method_source>, [">= 0"])
76
+ s.add_dependency(%q<rgl>, [">= 0"])
77
+ s.add_dependency(%q<pry>, [">= 0"])
78
+ s.add_dependency(%q<pry-doc>, [">= 0"])
55
79
  s.add_dependency(%q<rspec>, [">= 0"])
56
80
  s.add_dependency(%q<jeweler>, [">= 0"])
57
81
  end
data/lib/kefka.rb CHANGED
@@ -1,14 +1,117 @@
1
- module Kefka
1
+ require 'coderay'
2
+ require 'rgl/adjacency'
3
+ require 'rgl/dot'
4
+ require 'method_source'
2
5
 
3
- @@values = {}
4
- @@method_source = {}
6
+ require 'forwardable'
7
+ require 'logger'
5
8
 
6
- class << self
9
+ class Kefka
7
10
 
8
- def get_values_of_locals_from_binding(binding)
9
- locals = binding.eval("local_variables")
11
+ class Method
12
+
13
+ attr_reader :classname, :id, :file, :line, :caller
14
+ attr_accessor :format
15
+
16
+ def initialize(options={})
17
+ @classname = options[:classname]
18
+ @id = options[:id]
19
+ @file = options[:file]
20
+ @line = options[:line]
21
+ @format = options[:format] || :plain
22
+ end
23
+
24
+ def source_location
25
+ return nil unless @file && @line
26
+ [@file,@line]
27
+ end
28
+
29
+ def end_line
30
+ return nil unless @line && source
31
+ @line + source.lines.count - 1
32
+ end
33
+
34
+ def key
35
+ source_location ? source_location.join(":") : nil
36
+ end
37
+
38
+ def contains?(file, line)
39
+ @file == file && @line < line && end_line > line
40
+ end
41
+
42
+ def source
43
+ @source ||= begin
44
+ MethodSource.source_helper(source_location)
45
+ rescue MethodSource::SourceNotFoundError => e
46
+ warn "Warning: #{e.class} #{e.message}"
47
+ nil
48
+ end
49
+ end
50
+
51
+ def formatted_source
52
+ if @format == :html
53
+ CodeRay.scan(source, :ruby)
54
+ .div(:line_numbers => :table, :line_number_start => @line)
55
+ else
56
+ source
57
+ end
58
+ end
59
+
60
+ def to_s
61
+ "#{classname} #{id}"
62
+ end
63
+
64
+ def to_json(*a)
65
+ {
66
+ :classname => @classname,
67
+ :id => @id,
68
+ :file => @file,
69
+ :line => @line,
70
+ :end_line => end_line,
71
+ :source => formatted_source
72
+ }.to_json(*a)
73
+ end
74
+ end
75
+
76
+ class MethodGraph
77
+ extend Forwardable
78
+ def_delegators :@graph, :vertices, :edges, :add_edge,
79
+ :write_to_graphic_file
80
+
81
+ def initialize
82
+ @graph = RGL::DirectedAdjacencyGraph.new
83
+ end
84
+
85
+ def to_json
86
+ {
87
+ :vertices => vertices,
88
+ :edges => edges.map { |edge|
89
+ {
90
+ :source => edge.source.key,
91
+ :target => edge.target.key
92
+ }
93
+ }
94
+ }.to_json
95
+ end
96
+ end
97
+
98
+ class Tracer
99
+
100
+ attr_reader :method_table, :local_values, :logger, :callstack, :method_graph
101
+
102
+ def initialize(log_level = Logger::INFO)
103
+ @method_graph = MethodGraph.new
104
+ @callstack = []
105
+ @local_values = {}
106
+ @event_disable = []
107
+ @logger = Logger.new($stderr)
108
+ @logger.level = log_level
109
+ end
110
+
111
+ def get_values_of_locals_from_binding(target)
112
+ locals = target.eval("local_variables")
10
113
  locals.inject({}) do |result,l|
11
- val = binding.eval(l.to_s)
114
+ val = target.eval(l.to_s)
12
115
  val = begin
13
116
  # deep copy
14
117
  Marshal.load(Marshal.dump(val)) if val
@@ -21,100 +124,85 @@ module Kefka
21
124
  end
22
125
  end
23
126
 
24
- # Things to IGNORE
25
- # 1. loading of rubygems/libraries
127
+ def disable_event_handlers?
128
+ !@event_disable.empty?
129
+ end
130
+
26
131
  def callgraph_handler(event, file, line, id, binding, classname)
27
- # do not trace current file (TODO: and anything in this library)
28
132
  return if file == __FILE__
133
+
134
+ @logger.debug "#{event} - #{file}:#{line} #{classname} #{id}"
135
+
136
+ if disable_event_handlers?
137
+ @event_disable.pop if event == "end"
138
+ return
139
+ end
140
+
29
141
  case event
142
+ when "class"
143
+ @event_disable << true
30
144
  when "call"
31
- # mark the start of method call
32
-
33
- # key must be uniquely identifiable -
34
- # Class methodname is not enough
35
- # perhaps include:
36
- # 1. line
37
- # 2. file
38
- key = "#{classname}_#{id}"
39
- @@method_source[key] = [file, caller[1], line]
40
- when "line"
145
+ method_caller = @callstack.last
146
+
147
+ method = Method.new(
148
+ :classname => classname,
149
+ :id => id,
150
+ :file => file,
151
+ :line => line
152
+ )
153
+
154
+ if method_caller
155
+ @method_graph.add_edge(method_caller,method)
156
+ end
157
+
158
+ @callstack << method
41
159
  when "return"
42
- key = "#{classname}_#{id}"
43
- @@method_source[key] << line if @@method_source[key]
160
+ @callstack.pop
44
161
  else
45
162
  # do nothing
46
163
  end
47
164
  rescue Exception => e
48
- puts "#{e.message} from -- #{e.backtrace.join("\n")}"
165
+ puts "\n#{e.class}: #{e.message} \n from #{e.backtrace.join("\n")}"
166
+ Process.kill("KILL", $$)
49
167
  end
50
168
 
51
- def locals_values_handler(event, file, line, id, binding, classname)
52
-
169
+ def local_values_handler(event, file, line, id, binding, classname)
170
+ # skip variables that should not be tracked
171
+ # 1. anything in current lib (i.e __FILE__)
172
+ # 2. all local variables that are in TOP LEVEL BINDING before tracing
173
+ # - but these variables may be overwritten by the traced program,
174
+ # excluding them would mean not displaying certain relevant
175
+ # vars in that program
53
176
  return if file == __FILE__
54
177
 
178
+ @logger.debug "#{event} - #{file}:#{line} #{classname} #{id}" if $DEBUG
179
+
180
+ if disable_event_handlers?
181
+ @event_disable.pop if event == "end"
182
+ return
183
+ end
184
+
55
185
  case event
56
- when "call"
57
- #puts "Entering method #{classname} - #{id}"
186
+ when "class"
187
+ @event_disable << true
58
188
  when "line"
59
- # variables that should not be tracked
60
- # 1. anything in current lib
61
- # 2. all local variables that are in TOP LEVEL BINDING before tracing
62
- # - but these variables may be overwritten by the traced program,
63
- # excluding them would mean not displaying certain relevant
64
- # vars in that program
65
- key = "#{file}_#{line}".to_sym
66
- @@values[key] = get_values_of_locals_from_binding(binding)
189
+ key = [file, line].join(":")
190
+ @local_values[key] = get_values_of_locals_from_binding(binding)
67
191
  else
68
192
  # do nothing
69
193
  end
70
194
  end
71
195
 
72
- def start(handler)
73
- set_trace_func method(handler).to_proc
74
- end
75
-
76
- def stop
77
- set_trace_func nil
78
- end
79
-
80
- def trace(file, handler = :callgraph_handler)
81
- puts "\nTracing Execution using #{handler}...\n\n"
82
- start(handler)
83
- file.rewind if file.eof?
84
- code = file.read
85
- eval(code, TOPLEVEL_BINDING, file.path, 1)
86
- stop
87
- end
88
-
89
- def method_graph
90
- @@method_source
91
- end
92
-
93
- def display
94
- puts "\n==== Generating Method Callgraph...\n\n"
95
- @@method_source.each do |meth,props|
96
- puts
97
- puts meth
98
- puts
99
-
100
- file, parent_caller, start_line, finish_line = props
101
-
102
- File.open(file) { |f|
103
- (start_line - 1).times { f.readline }
104
-
105
- code = ""
106
- (finish_line - start_line + 1).times {
107
- code << f.readline
108
- }
109
- puts code
110
- }
196
+ def trace(file_path, handler = :callgraph_handler)
197
+ file = File.open(file_path)
111
198
 
112
- puts
113
- end
114
- end
199
+ thread = Thread.new {
200
+ code = file.read
201
+ eval(code, TOPLEVEL_BINDING, file.path, 1)
202
+ }
115
203
 
116
- def values
117
- @@values
204
+ thread.set_trace_func method(handler).to_proc
205
+ thread.join
118
206
  end
119
207
 
120
208
  end