kefka 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +8 -0
- data/README +4 -5
- data/TODO +4 -0
- data/VERSION +1 -1
- data/app.rb +36 -0
- data/bin/kefka +3 -9
- data/examples/sample_a.rb +61 -0
- data/kefka.gemspec +30 -6
- data/lib/kefka.rb +167 -79
- data/public/javascripts/app.js +115 -0
- data/public/javascripts/jquery-1.7.2.js +9404 -0
- data/public/stylesheets/application.css +14 -0
- data/views/index.erb +21 -0
- metadata +90 -10
- data/README.rdoc +0 -19
- data/examples/sample1.rb +0 -28
data/Gemfile
CHANGED
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.
|
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
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
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
|
-
|
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
|
-
|
14
|
-
|
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.
|
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-
|
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/
|
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
|
-
|
1
|
+
require 'coderay'
|
2
|
+
require 'rgl/adjacency'
|
3
|
+
require 'rgl/dot'
|
4
|
+
require 'method_source'
|
2
5
|
|
3
|
-
|
4
|
-
|
6
|
+
require 'forwardable'
|
7
|
+
require 'logger'
|
5
8
|
|
6
|
-
|
9
|
+
class Kefka
|
7
10
|
|
8
|
-
|
9
|
-
|
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 =
|
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
|
-
|
25
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
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
|
165
|
+
puts "\n#{e.class}: #{e.message} \n from #{e.backtrace.join("\n")}"
|
166
|
+
Process.kill("KILL", $$)
|
49
167
|
end
|
50
168
|
|
51
|
-
def
|
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 "
|
57
|
-
|
186
|
+
when "class"
|
187
|
+
@event_disable << true
|
58
188
|
when "line"
|
59
|
-
|
60
|
-
|
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
|
73
|
-
|
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
|
-
|
113
|
-
|
114
|
-
|
199
|
+
thread = Thread.new {
|
200
|
+
code = file.read
|
201
|
+
eval(code, TOPLEVEL_BINDING, file.path, 1)
|
202
|
+
}
|
115
203
|
|
116
|
-
|
117
|
-
|
204
|
+
thread.set_trace_func method(handler).to_proc
|
205
|
+
thread.join
|
118
206
|
end
|
119
207
|
|
120
208
|
end
|