vistual_call 1.0.0
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.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +19 -0
- data/LICENSE.txt +21 -0
- data/README.md +76 -0
- data/Rakefile +4 -0
- data/example/example.png +0 -0
- data/example/sample.rb +16 -0
- data/example/sinatra.png +0 -0
- data/example/sinatra.rb +8 -0
- data/lib/vistual_call/graph.rb +298 -0
- data/lib/vistual_call/method_call_tree.rb +39 -0
- data/lib/vistual_call/method_node.rb +48 -0
- data/lib/vistual_call/tracer.rb +29 -0
- data/lib/vistual_call/version.rb +5 -0
- data/lib/vistual_call.rb +22 -0
- data/read_yaml.rb +6 -0
- data/sig/vistual_call.rbs +4 -0
- data/theme.yml +86 -0
- data/vistual_call.gemspec +44 -0
- metadata +66 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 254b90d7ec0cbe6c4c202202beaf927acab31c4a8ebdf38c566affefde719513
|
4
|
+
data.tar.gz: bb2dabaf7f79a194341c6757a0e94e48e66695460b6f47333595d6ce6aa47f2a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7ddce952a828984266dd861a20bce14942ec483b9e5a2edd864fe93fb47c3e04cdad8f835cd59a5b11f4bb0d84fffe925be8b77e3de5d7c63261cd31e3584669
|
7
|
+
data.tar.gz: 11179df1c70d10ab271c4754e1986e09d4c7cc49887bca333ccc10a9880f83c808db525d6462418d0c83e57240960a821b691f5a15a080c44ee8b9b11d0fc6e5
|
data/.DS_Store
ADDED
Binary file
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2023 Mark24
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# VistualCall
|
2
|
+
|
3
|
+
VistualCall is a gem to help you trace your code and export beautiful vistual call graph.
|
4
|
+
|
5
|
+
# Introduction
|
6
|
+
|
7
|
+
## Dependency
|
8
|
+
|
9
|
+
1. Graphviz
|
10
|
+
|
11
|
+
You need to install [Graphviz](https://graphviz.org/) by yourself.
|
12
|
+
|
13
|
+
Go to install [graphviz](https://graphviz.org/download/).
|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
### 1. Install gem
|
18
|
+
|
19
|
+
`gem install vistual_call`
|
20
|
+
|
21
|
+
### 2. Only the method needs to be wrapped.
|
22
|
+
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
require 'vistual_call'
|
26
|
+
|
27
|
+
def call_c
|
28
|
+
end
|
29
|
+
|
30
|
+
def call_b
|
31
|
+
call_c
|
32
|
+
end
|
33
|
+
|
34
|
+
def call_a
|
35
|
+
call_b
|
36
|
+
end
|
37
|
+
|
38
|
+
VistualCall.trace do
|
39
|
+
call_a # enter call
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+

|
44
|
+
|
45
|
+
The method after each node is call order number. This will help your understand the order of the function call.
|
46
|
+
|
47
|
+
## 3. More information
|
48
|
+
|
49
|
+
## configuration
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
# you can pass options
|
53
|
+
VistualCall.trace(options) do
|
54
|
+
# run your code here...
|
55
|
+
end
|
56
|
+
```
|
57
|
+
|
58
|
+
Options:
|
59
|
+
|
60
|
+
| name | type | required | explain | example |
|
61
|
+
| ---- | ---- | ---- | ---- | ---- |
|
62
|
+
| label | String | true | 标题 | Hello |
|
63
|
+
| labelloc | Symbol | false | 标题位置: :top :bottom :center | :top |
|
64
|
+
| labeljust | Symbol | false | 标题对齐位置 :left, :center, :right | :center |
|
65
|
+
| direction | Symbol| false | 绘制方向,依次是 :TB(从上到下),:LR(从左到右,默认方式),:BT(从下到上),:RL(从右到左) | :LR |
|
66
|
+
| format | String | false | 输出图片格式,查看 [graphviz 支持输出格式](https://graphviz.org/docs/outputs/) 'png'、'svg' | 默认 'png' |
|
67
|
+
| output | String | false | 导出图片绝对路径 | 默认家目录下 `vistual_call_result.png` |
|
68
|
+
| theme | Symbol | false | 配色主题 :sky, :lemon | 默认 :sky |
|
69
|
+
| show_dot | boolean | false | 展示 dot 内容 | 默认 false |
|
70
|
+
| show_order_number | boolean | false | 输出调用序号 | 默认 true |
|
71
|
+
| jump_list | Array(String) | false | 跳过节点,默认 ["Kernel#class", "Kernel#frozen?"] | - |
|
72
|
+
| heightlight_match | Regex | false | 默认高亮匹配 label, 默认 /method_missing/ | /method_missing/ |
|
73
|
+
|
74
|
+
## LICENSE
|
75
|
+
|
76
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/example/example.png
ADDED
Binary file
|
data/example/sample.rb
ADDED
data/example/sinatra.png
ADDED
Binary file
|
data/example/sinatra.rb
ADDED
@@ -0,0 +1,298 @@
|
|
1
|
+
require "set"
|
2
|
+
require "tempfile"
|
3
|
+
require "yaml"
|
4
|
+
require_relative "./tracer"
|
5
|
+
|
6
|
+
module VistualCall
|
7
|
+
# Output config
|
8
|
+
DEFAULT_OUTPUT_FORMAT = "png"
|
9
|
+
DEFAULT_OUTPUT = "vistual_call_result.png"
|
10
|
+
DEFAULT_OUTPUT_PATH = File.join(Dir.home, DEFAULT_OUTPUT)
|
11
|
+
|
12
|
+
# Jump Node
|
13
|
+
DEFAULT_JUMP_NODE = %w[Kernel#class Kernel#frozen?]
|
14
|
+
|
15
|
+
# Hightlight Label
|
16
|
+
HIGHT_LIGHT_REGEX = /method_missing/
|
17
|
+
|
18
|
+
# Core
|
19
|
+
DIRECTIONS = %w[TB LR BT RL]
|
20
|
+
LABEL_LOC = %w[top bottom center]
|
21
|
+
LABEL_JUST = %w[left right center]
|
22
|
+
|
23
|
+
class Graph
|
24
|
+
@@custer_count = 0
|
25
|
+
attr_accessor :call_tree_root
|
26
|
+
|
27
|
+
def self.root
|
28
|
+
File.expand_path("../../", __dir__)
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(options = {})
|
32
|
+
@label = options.fetch(:label, nil)
|
33
|
+
@labelloc = options.fetch(:labelloc, :top).to_s
|
34
|
+
if !LABEL_LOC.include?(@labelloc)
|
35
|
+
raise VistualCallError("labelloc must in #{LABEL_LOC}")
|
36
|
+
end
|
37
|
+
@labeljust = options.fetch(:labeljust, :center).to_s
|
38
|
+
if !LABEL_JUST.include?(@labeljust)
|
39
|
+
raise VistualCallError("labeljust must in #{LABEL_JUST}")
|
40
|
+
end
|
41
|
+
@margin = options[:margin] || "5"
|
42
|
+
# display config
|
43
|
+
@direction = options.fetch(:direction, :LR).to_s
|
44
|
+
if !DIRECTIONS.include?(@direction)
|
45
|
+
raise VistualCallError("direction must in #{DIRECTIONS}")
|
46
|
+
end
|
47
|
+
@format = options.fetch(:format, DEFAULT_OUTPUT_FORMAT)
|
48
|
+
@output = File.expand_path(options.fetch(:output, DEFAULT_OUTPUT_PATH))
|
49
|
+
|
50
|
+
@show_dot = options.fetch(:show_dot, false)
|
51
|
+
@show_order_number = options.fetch(:show_order_number, true)
|
52
|
+
|
53
|
+
# node graph config
|
54
|
+
@jump_list = options.fetch(:jump_list, DEFAULT_JUMP_NODE)
|
55
|
+
@heightlight_match = options.fetch(:heightlight_match, HIGHT_LIGHT_REGEX)
|
56
|
+
|
57
|
+
# theme
|
58
|
+
@theme_name = options.fetch(:theme, :sky).to_s
|
59
|
+
@theme_config =
|
60
|
+
YAML.load_file(File.join(self.class.root, "theme.yml"), aliases: true)
|
61
|
+
@theme =
|
62
|
+
@theme_config[@theme_name] || @theme_config[@theme_config["use_theme"]]
|
63
|
+
@node_attrs = @theme["node_attrs"]
|
64
|
+
@edge_attrs = @theme["edge_attrs"]
|
65
|
+
@node_waring_attrs = @theme["node_warn_attrs"]
|
66
|
+
|
67
|
+
# working cache
|
68
|
+
@tracer = Tracer.new
|
69
|
+
|
70
|
+
@call_tree_root = nil
|
71
|
+
@call_tree_hashmap = nil
|
72
|
+
|
73
|
+
@label_hashmap = {}
|
74
|
+
@cache_graph_nodes_set = Set.new
|
75
|
+
@cache_graph_edges = []
|
76
|
+
end
|
77
|
+
|
78
|
+
def get_call_tree_root
|
79
|
+
@call_tree_root = @tracer.call_tree_root
|
80
|
+
end
|
81
|
+
|
82
|
+
def get_call_tree_hashmap
|
83
|
+
@call_tree_hashmap = @tracer.call_tree_hashmap
|
84
|
+
end
|
85
|
+
|
86
|
+
def track(&block)
|
87
|
+
@tracer.track(&block)
|
88
|
+
end
|
89
|
+
|
90
|
+
def get_graph_node_id(node)
|
91
|
+
label_name = node.method_name
|
92
|
+
if !@label_hashmap.key?(label_name)
|
93
|
+
@label_hashmap[label_name] = node.node_id
|
94
|
+
end
|
95
|
+
return @label_hashmap[label_name]
|
96
|
+
end
|
97
|
+
|
98
|
+
def collect_graph_nodes_edges(node)
|
99
|
+
return if node == nil
|
100
|
+
return if @jump_list.include?(node.method_name)
|
101
|
+
|
102
|
+
graph_node_id = get_graph_node_id(node)
|
103
|
+
|
104
|
+
# build node
|
105
|
+
@cache_graph_nodes_set.add(graph_node_id)
|
106
|
+
|
107
|
+
# buid edges
|
108
|
+
if node.parent_node_id
|
109
|
+
parent_node = @call_tree_hashmap[node.parent_node_id]
|
110
|
+
parent_graph_node_id = get_graph_node_id(parent_node)
|
111
|
+
@cache_graph_edges.push([parent_graph_node_id, graph_node_id])
|
112
|
+
end
|
113
|
+
|
114
|
+
# Make recurse call
|
115
|
+
if node.children.size > 0
|
116
|
+
node.children.each do |one_child_node|
|
117
|
+
collect_graph_nodes_edges(one_child_node)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def create_or_set(obj, key, value)
|
123
|
+
obj[key] = [] if !obj.key?(key)
|
124
|
+
obj[key].push(value)
|
125
|
+
end
|
126
|
+
|
127
|
+
def collect_group_cluster
|
128
|
+
@cluster_group = {}
|
129
|
+
@label_hashmap.keys.each do |label_name|
|
130
|
+
if label_name.count("::") == 0
|
131
|
+
create_or_set(@cluster_group, "_single", @label_hashmap[label_name])
|
132
|
+
else
|
133
|
+
module_name = label_name.split("#")
|
134
|
+
module_name.pop
|
135
|
+
module_name = module_name.join("#")
|
136
|
+
create_or_set(@cluster_group, module_name, @label_hashmap[label_name])
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def get_dot_config_string(hashmap = nil)
|
142
|
+
return if !hashmap
|
143
|
+
config = hashmap.keys.map { |key| "#{key}=\"#{hashmap[key]}\"" }.join(",")
|
144
|
+
return "[#{config}]"
|
145
|
+
end
|
146
|
+
|
147
|
+
def merge_config(*configs)
|
148
|
+
new_config = {}
|
149
|
+
configs.each { |conf| new_config = new_config.merge(conf) }
|
150
|
+
return new_config
|
151
|
+
end
|
152
|
+
|
153
|
+
def get_label_text(node)
|
154
|
+
result = "#{node.defined_class}##{node.method_id}"
|
155
|
+
result << " (#{node.node_id})" if @show_order_number
|
156
|
+
return result
|
157
|
+
end
|
158
|
+
|
159
|
+
def dot_node_format(node_id)
|
160
|
+
if node_id == StartNodeID
|
161
|
+
return("node#{node_id}#{get_dot_config_string({ label: "Start" })}")
|
162
|
+
end
|
163
|
+
|
164
|
+
node = @call_tree_hashmap[node_id]
|
165
|
+
|
166
|
+
config = { label: get_label_text(node) }
|
167
|
+
config = merge_config(config, @node_waring_attrs) if @heightlight_match =~
|
168
|
+
node.method_name
|
169
|
+
|
170
|
+
return("node#{node_id}#{get_dot_config_string(config)}")
|
171
|
+
end
|
172
|
+
|
173
|
+
def generate_node_text(graph_node_id)
|
174
|
+
return dot_node_format(graph_node_id) + ";\n"
|
175
|
+
end
|
176
|
+
|
177
|
+
def generate_cluster(module_name, graph_ids)
|
178
|
+
@@custer_count += 1
|
179
|
+
|
180
|
+
cluster_style_config = @theme.dig("cluster", "style") || nil
|
181
|
+
cluster_style_config_text =
|
182
|
+
cluster_style_config && "style=\"#{cluster_style_config}\";"
|
183
|
+
|
184
|
+
cluster_color_config = @theme.dig("cluster", "color") || nil
|
185
|
+
cluster_color_config_text =
|
186
|
+
cluster_color_config && "color=\"#{cluster_color_config}\";"
|
187
|
+
|
188
|
+
cluster_node_config = @theme.dig("cluster_node") || nil
|
189
|
+
cluster_node_config_text =
|
190
|
+
cluster_node_config &&
|
191
|
+
"node#{get_dot_config_string(cluster_node_config)};"
|
192
|
+
|
193
|
+
template = <<-CLUSTER
|
194
|
+
subgraph cluster_#{@@custer_count} {
|
195
|
+
label="#{module_name}";
|
196
|
+
#{cluster_style_config_text}
|
197
|
+
#{cluster_color_config_text}
|
198
|
+
#{cluster_node_config_text}
|
199
|
+
|
200
|
+
#{graph_ids.map { |graph_node_id| generate_node_text(graph_node_id) }.join("")}
|
201
|
+
}
|
202
|
+
CLUSTER
|
203
|
+
|
204
|
+
return template
|
205
|
+
end
|
206
|
+
|
207
|
+
def render_nodes_and_clusters()
|
208
|
+
content = ""
|
209
|
+
@cluster_group.keys.each do |key|
|
210
|
+
if key == "_single"
|
211
|
+
graph_node_ids = @cluster_group[key]
|
212
|
+
|
213
|
+
graph_node_ids.each do |graph_node_id|
|
214
|
+
content << generate_node_text(graph_node_id)
|
215
|
+
end
|
216
|
+
else
|
217
|
+
module_name = key
|
218
|
+
module_graph_node_ids = @cluster_group[key]
|
219
|
+
|
220
|
+
content << generate_cluster(module_name, module_graph_node_ids)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
return content
|
225
|
+
end
|
226
|
+
|
227
|
+
def dot_edge_format(edge)
|
228
|
+
parent_id, child_id = edge
|
229
|
+
return("node#{parent_id} -> node#{child_id}")
|
230
|
+
end
|
231
|
+
|
232
|
+
def render_edges
|
233
|
+
@cache_graph_edges.map { |edge| dot_edge_format(edge) }.join("\n")
|
234
|
+
end
|
235
|
+
|
236
|
+
def render_graph_config
|
237
|
+
@theme.dig("graph") &&
|
238
|
+
"graph #{get_dot_config_string(@theme.dig("graph"))}"
|
239
|
+
end
|
240
|
+
|
241
|
+
def render_node_config
|
242
|
+
@node_attrs && "node #{get_dot_config_string(@node_attrs)};"
|
243
|
+
end
|
244
|
+
|
245
|
+
def render_edge_config
|
246
|
+
@edge_attrs && "edge #{get_dot_config_string(@edge_attrs)};"
|
247
|
+
end
|
248
|
+
|
249
|
+
def render_meta_info
|
250
|
+
meta_info = ["rankdir=#{@direction};"]
|
251
|
+
meta_info << "margin=#{@margin};" if @margin
|
252
|
+
meta_info << "label=\"#{@label}\";" if @label
|
253
|
+
meta_info << "labelloc=\"#{@labelloc}\";" if @labelloc
|
254
|
+
meta_info << "labeljust=\"#{@labeljust}\";" if @labeljust
|
255
|
+
|
256
|
+
meta_info.join("\n")
|
257
|
+
end
|
258
|
+
def generate_dot_template
|
259
|
+
dot_template = <<-DOT
|
260
|
+
digraph "virtual_call_graph"{
|
261
|
+
|
262
|
+
#{render_meta_info}
|
263
|
+
#{render_graph_config}
|
264
|
+
#{render_node_config}
|
265
|
+
#{render_edge_config}
|
266
|
+
|
267
|
+
#{render_nodes_and_clusters}
|
268
|
+
|
269
|
+
#{render_edges}
|
270
|
+
|
271
|
+
}
|
272
|
+
DOT
|
273
|
+
|
274
|
+
return dot_template
|
275
|
+
end
|
276
|
+
|
277
|
+
def create_dot_file(content)
|
278
|
+
dot_file_path = nil
|
279
|
+
dot_file = Tempfile.new("vistual_call")
|
280
|
+
dot_file_path = dot_file.path
|
281
|
+
dot_file.write content
|
282
|
+
dot_file.close
|
283
|
+
return dot_file_path
|
284
|
+
end
|
285
|
+
|
286
|
+
def output
|
287
|
+
get_call_tree_root()
|
288
|
+
get_call_tree_hashmap()
|
289
|
+
|
290
|
+
collect_graph_nodes_edges(@call_tree_root)
|
291
|
+
collect_group_cluster()
|
292
|
+
content = generate_dot_template()
|
293
|
+
dot_file_path = create_dot_file(content)
|
294
|
+
system("cat #{dot_file_path}") if @show_dot
|
295
|
+
system("dot #{dot_file_path} -T #{@format} -o '#{@output}'")
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require_relative "./method_node"
|
2
|
+
|
3
|
+
module VistualCall
|
4
|
+
class MethodCallTree
|
5
|
+
attr_accessor :call_stack, :memo
|
6
|
+
def initialize
|
7
|
+
@call_stack = []
|
8
|
+
@memo = {}
|
9
|
+
|
10
|
+
root = BasicNode.new
|
11
|
+
@call_stack.push(root)
|
12
|
+
memo_it(root.node_id, root)
|
13
|
+
end
|
14
|
+
|
15
|
+
def memo_it(node_id, payload)
|
16
|
+
@memo[node_id] = payload unless @memo.key?(node_id)
|
17
|
+
return @memo[node_id]
|
18
|
+
end
|
19
|
+
|
20
|
+
def dispatch_call(method_info)
|
21
|
+
# Memo: 构建树的call、return 不应该被跳过、修改,来保持对应关系,可以还原起调用树。过滤工作应该在消费数据的层面
|
22
|
+
node = MethodNode.new(method_info)
|
23
|
+
node_id = node.node_id
|
24
|
+
memo_it(node_id, node)
|
25
|
+
|
26
|
+
@call_stack.push(node)
|
27
|
+
end
|
28
|
+
|
29
|
+
def dispatch_return(method_info = {})
|
30
|
+
match_call_method = @call_stack.pop
|
31
|
+
|
32
|
+
match_call_method_parent = @call_stack.last
|
33
|
+
if match_call_method_parent
|
34
|
+
match_call_method.parent_node_id = match_call_method_parent.node_id
|
35
|
+
match_call_method_parent.add_child(match_call_method)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module VistualCall
|
2
|
+
StartNodeID = 1
|
3
|
+
|
4
|
+
class BasicNode
|
5
|
+
attr_accessor :node_id, :method_name, :parent_node_id, :children
|
6
|
+
def initialize(method_name = nil)
|
7
|
+
@node_id = StartNodeID
|
8
|
+
@method_name = method_name || "Start"
|
9
|
+
@parent_node_id = nil
|
10
|
+
@children = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def add_child(child)
|
14
|
+
@children << child
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class MethodNode
|
19
|
+
@@instance_count = StartNodeID
|
20
|
+
|
21
|
+
def self.instance_count
|
22
|
+
@@instance_count
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.get_method_name(node_info)
|
26
|
+
"#{node_info.defined_class}##{node_info.method_id}"
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_accessor :node_id, :method_name, :parent_node_id, :children
|
30
|
+
def initialize(method_info)
|
31
|
+
@@instance_count += 1
|
32
|
+
@node_id = @@instance_count
|
33
|
+
@parent_node_id = nil
|
34
|
+
|
35
|
+
# Must copy TracePointer information, because TracePointer cannot access outside.
|
36
|
+
%i[path event lineno method_id defined_class parameters].each do |attr|
|
37
|
+
instance_variable_set("@#{attr}", method_info.send(attr))
|
38
|
+
self.class.send(:attr_accessor, attr)
|
39
|
+
end
|
40
|
+
@method_name = self.class.get_method_name(self)
|
41
|
+
@children = []
|
42
|
+
end
|
43
|
+
|
44
|
+
def add_child(child)
|
45
|
+
@children << child
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require_relative "./method_call_tree"
|
2
|
+
|
3
|
+
module VistualCall
|
4
|
+
class Tracer
|
5
|
+
attr_accessor :call_tree
|
6
|
+
def initialize(options = {})
|
7
|
+
@events = %i[call return]
|
8
|
+
@call_tree = MethodCallTree.new
|
9
|
+
|
10
|
+
@trace_point =
|
11
|
+
TracePoint.new(*@events) do |tp|
|
12
|
+
# TODO: stop & disable situations
|
13
|
+
@call_tree.send("dispatch_#{tp.event}", tp)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def track(&block)
|
18
|
+
@trace_point.enable { block.call }
|
19
|
+
end
|
20
|
+
|
21
|
+
def call_tree_root
|
22
|
+
@call_tree.call_stack.first || nil
|
23
|
+
end
|
24
|
+
|
25
|
+
def call_tree_hashmap
|
26
|
+
@call_tree.memo || {}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/vistual_call.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "vistual_call/version"
|
4
|
+
require_relative "./vistual_call/graph"
|
5
|
+
|
6
|
+
module VistualCall
|
7
|
+
class VistualCallError < StandardError
|
8
|
+
end
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def trace(options = {})
|
12
|
+
unless block_given?
|
13
|
+
puts "Block required!"
|
14
|
+
return
|
15
|
+
end
|
16
|
+
|
17
|
+
proxy = ::VistualCall::Graph.new(options)
|
18
|
+
proxy.track { yield }
|
19
|
+
proxy.output
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/read_yaml.rb
ADDED
data/theme.yml
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
# Setting your theme
|
2
|
+
use_theme: "sky"
|
3
|
+
|
4
|
+
|
5
|
+
# Theme basic settings
|
6
|
+
basic_node: &basic_node
|
7
|
+
fontname: "wqy-microhei"
|
8
|
+
shape: "box"
|
9
|
+
peripheries: 1
|
10
|
+
style: "rounded, filled"
|
11
|
+
|
12
|
+
# Theme list
|
13
|
+
lemon:
|
14
|
+
name: "柠檬黄主题"
|
15
|
+
|
16
|
+
# 说明
|
17
|
+
# color 非填充是边框色、填充就是节点背景色
|
18
|
+
# fontcolor 是文字颜色
|
19
|
+
|
20
|
+
# 画布属性
|
21
|
+
graph:
|
22
|
+
bgcolor: "#eff1f3" # 全局背景
|
23
|
+
|
24
|
+
# 节点
|
25
|
+
node_attrs: &lemon_node
|
26
|
+
<<: *basic_node
|
27
|
+
color: "#696773"
|
28
|
+
fontcolor: "#fcfcfc"
|
29
|
+
|
30
|
+
# 高亮节点
|
31
|
+
node_warn_attrs:
|
32
|
+
<<: *basic_node
|
33
|
+
fontcolor: "#ffffff"
|
34
|
+
color: "#f19b97"
|
35
|
+
|
36
|
+
# 边
|
37
|
+
edge_attrs:
|
38
|
+
color: "#696773"
|
39
|
+
|
40
|
+
# 子图容器
|
41
|
+
cluster:
|
42
|
+
style: "filled"
|
43
|
+
color: "#e3efd7"
|
44
|
+
|
45
|
+
# 子图节点
|
46
|
+
cluster_node:
|
47
|
+
color: "#f4b184"
|
48
|
+
fontcolor: "#ffffff"
|
49
|
+
|
50
|
+
|
51
|
+
sky:
|
52
|
+
name: "天真蓝主题"
|
53
|
+
|
54
|
+
# 说明
|
55
|
+
# color 非填充是边框色、填充就是节点背景色
|
56
|
+
# fontcolor 是文字颜色
|
57
|
+
|
58
|
+
# 画布背景
|
59
|
+
graph:
|
60
|
+
bgcolor: "#ffffff" # 全局背景
|
61
|
+
|
62
|
+
# 节点
|
63
|
+
node_attrs:
|
64
|
+
<<: *basic_node
|
65
|
+
color: "#deeaf6"
|
66
|
+
fontcolor: "#000000"
|
67
|
+
|
68
|
+
# 高亮节点
|
69
|
+
node_warn_attrs:
|
70
|
+
<<: *basic_node
|
71
|
+
fontcolor: "#000000"
|
72
|
+
color: "#ffda65"
|
73
|
+
|
74
|
+
# 边
|
75
|
+
edge_attrs:
|
76
|
+
color: "#021d57"
|
77
|
+
|
78
|
+
# 子图
|
79
|
+
cluster:
|
80
|
+
style: "filled"
|
81
|
+
color: "#deeaf6"
|
82
|
+
|
83
|
+
# 子图节点
|
84
|
+
cluster_node:
|
85
|
+
color: "#5a9ad7"
|
86
|
+
fontcolor: "#ffffff"
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/vistual_call/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "vistual_call"
|
7
|
+
spec.version = VistualCall::VERSION
|
8
|
+
spec.authors = ["Mark24"]
|
9
|
+
spec.email = ["mark.zhangyoung@qq.com"]
|
10
|
+
|
11
|
+
spec.summary = "Export vistual call graph"
|
12
|
+
spec.description = "Make export vistual call graph easy to use."
|
13
|
+
spec.homepage = "https://github.com/Mark24Code/vistual_call"
|
14
|
+
spec.required_ruby_version = ">= 2.6.0"
|
15
|
+
|
16
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
17
|
+
spec.metadata[
|
18
|
+
"source_code_uri"
|
19
|
+
] = "https://github.com/Mark24Code/vistual_call"
|
20
|
+
spec.metadata["changelog_uri"] = "https://github.com/Mark24Code/vistual_call"
|
21
|
+
|
22
|
+
# Specify which files should be added to the gem when it is released.
|
23
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
24
|
+
spec.files =
|
25
|
+
Dir.chdir(__dir__) do
|
26
|
+
`git ls-files -z`.split("\x0")
|
27
|
+
.reject do |f|
|
28
|
+
(File.expand_path(f) == __FILE__) ||
|
29
|
+
f.start_with?(
|
30
|
+
*%w[bin/ test/ spec/ features/ .git .circleci appveyor]
|
31
|
+
)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
spec.bindir = "exe"
|
35
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
36
|
+
spec.require_paths = ["lib"]
|
37
|
+
spec.licenses = ["MIT"]
|
38
|
+
|
39
|
+
# Uncomment to register a new dependency of your gem
|
40
|
+
# spec.add_dependency "example-gem", "~> 1.0"
|
41
|
+
|
42
|
+
# For more information and examples about making a new gem, check out our
|
43
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
44
|
+
end
|
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: vistual_call
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mark24
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-04-24 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Make export vistual call graph easy to use.
|
14
|
+
email:
|
15
|
+
- mark.zhangyoung@qq.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- ".DS_Store"
|
21
|
+
- Gemfile
|
22
|
+
- Gemfile.lock
|
23
|
+
- LICENSE.txt
|
24
|
+
- README.md
|
25
|
+
- Rakefile
|
26
|
+
- example/example.png
|
27
|
+
- example/sample.rb
|
28
|
+
- example/sinatra.png
|
29
|
+
- example/sinatra.rb
|
30
|
+
- lib/vistual_call.rb
|
31
|
+
- lib/vistual_call/graph.rb
|
32
|
+
- lib/vistual_call/method_call_tree.rb
|
33
|
+
- lib/vistual_call/method_node.rb
|
34
|
+
- lib/vistual_call/tracer.rb
|
35
|
+
- lib/vistual_call/version.rb
|
36
|
+
- read_yaml.rb
|
37
|
+
- sig/vistual_call.rbs
|
38
|
+
- theme.yml
|
39
|
+
- vistual_call.gemspec
|
40
|
+
homepage: https://github.com/Mark24Code/vistual_call
|
41
|
+
licenses:
|
42
|
+
- MIT
|
43
|
+
metadata:
|
44
|
+
homepage_uri: https://github.com/Mark24Code/vistual_call
|
45
|
+
source_code_uri: https://github.com/Mark24Code/vistual_call
|
46
|
+
changelog_uri: https://github.com/Mark24Code/vistual_call
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options: []
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: 2.6.0
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
requirements: []
|
62
|
+
rubygems_version: 3.4.12
|
63
|
+
signing_key:
|
64
|
+
specification_version: 4
|
65
|
+
summary: Export vistual call graph
|
66
|
+
test_files: []
|