rotoscope 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.clang-format +2 -0
- data/.github/PULL_REQUEST_TEMPLATE +3 -0
- data/.gitignore +7 -0
- data/.rubocop.yml +15 -0
- data/.ruby-version +1 -0
- data/Gemfile +3 -0
- data/LICENSE +22 -0
- data/README.md +215 -0
- data/Rakefile +36 -0
- data/bin/fmt +9 -0
- data/dev.yml +12 -0
- data/ext/rotoscope/callsite.c +78 -0
- data/ext/rotoscope/callsite.h +17 -0
- data/ext/rotoscope/extconf.rb +10 -0
- data/ext/rotoscope/rotoscope.c +391 -0
- data/ext/rotoscope/rotoscope.h +65 -0
- data/ext/rotoscope/stack.c +92 -0
- data/ext/rotoscope/stack.h +29 -0
- data/ext/rotoscope/strmemo.c +33 -0
- data/ext/rotoscope/strmemo.h +14 -0
- data/ext/rotoscope/tracepoint.c +8 -0
- data/ext/rotoscope/tracepoint.h +17 -0
- data/lib/rotoscope.rb +71 -0
- data/lib/uthash/uthash.h +1107 -0
- data/rotoscope.gemspec +23 -0
- data/test/fixture_inner.rb +10 -0
- data/test/fixture_outer.rb +10 -0
- data/test/monadify.rb +16 -0
- data/test/rotoscope_test.rb +482 -0
- metadata +129 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9e3e40d016f9db6e000338a2324aa7e841596075
|
4
|
+
data.tar.gz: b09d90253edea8dadcef671c33e11fb482e6e505
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5ff87cb9a2d13fc25ad797d14e4f38d92132de716ee164d201032c953f96ca47d1e268413cc5c5041b50e6cdf0f7e1477a41ead4ccea7e40da5190675bc99761
|
7
|
+
data.tar.gz: 6021e1a40637d82234fc87da1a66d93e2917fd5379da67692420c502b92f7fac1b81df3beffa24896c9492ce6cfdcd0d1c72d5223f3e6f4ddd9e174070df521f
|
data/.clang-format
ADDED
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
inherit_from:
|
2
|
+
- http://shopify.github.io/ruby-style-guide/rubocop.yml
|
3
|
+
|
4
|
+
AllCops:
|
5
|
+
Exclude:
|
6
|
+
- 'tmp/**/*'
|
7
|
+
TargetRubyVersion: 2.2
|
8
|
+
|
9
|
+
Metrics/LineLength:
|
10
|
+
Exclude:
|
11
|
+
- 'test/**/*'
|
12
|
+
|
13
|
+
Style/GlobalVars:
|
14
|
+
Exclude:
|
15
|
+
- 'ext/rotoscope/extconf.rb'
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.3.3
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright 2017 Shopify
|
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 all
|
13
|
+
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 THE
|
21
|
+
SOFTWARE.
|
22
|
+
|
data/README.md
ADDED
@@ -0,0 +1,215 @@
|
|
1
|
+
# Rotoscope
|
2
|
+
|
3
|
+
Rotoscope performs introspection of method calls in Ruby programs.
|
4
|
+
|
5
|
+
## Status [![status](https://circleci.com/gh/Shopify/rotoscope/tree/master.svg?style=shield&circle-token=cddbd315df7a81ab944adf4dfc14a5800cd589fc)](https://circleci.com/gh/Shopify/rotoscope/tree/master)
|
6
|
+
|
7
|
+
Alpha!
|
8
|
+
|
9
|
+
## Example
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
require 'rotoscope'
|
13
|
+
|
14
|
+
class Dog
|
15
|
+
def bark
|
16
|
+
Noisemaker.speak('woof!')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Noisemaker
|
21
|
+
def self.speak(str)
|
22
|
+
puts(str)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
log_file = File.expand_path('dog_trace.log')
|
27
|
+
puts "Writing to #{log_file}..."
|
28
|
+
|
29
|
+
Rotoscope.trace(log_file) do
|
30
|
+
dog1 = Dog.new
|
31
|
+
dog1.bark
|
32
|
+
end
|
33
|
+
```
|
34
|
+
|
35
|
+
The resulting method calls are saved in the specified `dest` in the order they were received.
|
36
|
+
|
37
|
+
Sample output:
|
38
|
+
|
39
|
+
```
|
40
|
+
event,entity,method_name,method_level,filepath,lineno
|
41
|
+
call,"Dog","new",class,"example/dog.rb",19
|
42
|
+
call,"Dog","initialize",instance,"example/dog.rb",19
|
43
|
+
return,"Dog","initialize",instance,"example/dog.rb",19
|
44
|
+
return,"Dog","new",class,"example/dog.rb",19
|
45
|
+
call,"Dog","bark",instance,"example/dog.rb",4
|
46
|
+
call,"Noisemaker","speak",class,"example/dog.rb",10
|
47
|
+
call,"Noisemaker","puts",class,"example/dog.rb",11
|
48
|
+
call,"IO","puts",instance,"example/dog.rb",11
|
49
|
+
call,"IO","write",instance,"example/dog.rb",11
|
50
|
+
return,"IO","write",instance,"example/dog.rb",11
|
51
|
+
call,"IO","write",instance,"example/dog.rb",11
|
52
|
+
return,"IO","write",instance,"example/dog.rb",11
|
53
|
+
return,"IO","puts",instance,"example/dog.rb",11
|
54
|
+
return,"Noisemaker","puts",class,"example/dog.rb",11
|
55
|
+
return,"Noisemaker","speak",class,"example/dog.rb",12
|
56
|
+
return,"Dog","bark",instance,"example/dog.rb",6
|
57
|
+
```
|
58
|
+
|
59
|
+
If you're interested solely in the flattened caller/callee list, you can pass the `flatten` option to retrieve that instead. This step will also remove all duplicate lines, which can produce significantly smaller output on large codebases.
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
# ... same code as above
|
63
|
+
|
64
|
+
Rotoscope.trace(log_file, flatten: true) do
|
65
|
+
dog1 = Dog.new
|
66
|
+
dog1.bark
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
Sample output:
|
71
|
+
|
72
|
+
```
|
73
|
+
entity,method_name,method_level,filepath,lineno,caller_entity,caller_method_name,caller_method_level
|
74
|
+
Dog,new,class,example/flattened_dog.rb,19,<ROOT>,<UNKNOWN>,<UNKNOWN>
|
75
|
+
Dog,initialize,instance,example/flattened_dog.rb,19,Dog,new,class
|
76
|
+
Dog,bark,instance,example/flattened_dog.rb,20,<ROOT>,<UNKNOWN>,<UNKNOWN>
|
77
|
+
Noisemaker,speak,class,example/flattened_dog.rb,5,Dog,bark,instance
|
78
|
+
Noisemaker,puts,class,example/flattened_dog.rb,11,Noisemaker,speak,class
|
79
|
+
IO,puts,instance,example/flattened_dog.rb,11,Noisemaker,puts,class
|
80
|
+
IO,write,instance,example/flattened_dog.rb,11,IO,puts,instance
|
81
|
+
```
|
82
|
+
|
83
|
+
## API
|
84
|
+
|
85
|
+
- [Public Class Methods](#public-class-methods)
|
86
|
+
- [`trace`](#rotoscopetracedest-blacklist--flatten-false)
|
87
|
+
- [`new`](#rotoscopenewdest-blacklist)
|
88
|
+
- [Public Instance Methods](#public-instance-methods)
|
89
|
+
- [`trace`](#rotoscopetraceblock)
|
90
|
+
- [`start_trace`](#rotoscopestart_trace)
|
91
|
+
- [`stop_trace`](#rotoscopestop_trace)
|
92
|
+
- [`mark`](#rotoscopemarkstr--)
|
93
|
+
- [`close`](#rotoscopeclose)
|
94
|
+
- [`state`](#rotoscopestate)
|
95
|
+
- [`closed?`](#rotoscopeclosed)
|
96
|
+
- [`log_path`](#rotoscopelog_path)
|
97
|
+
|
98
|
+
---
|
99
|
+
|
100
|
+
### Public Class Methods
|
101
|
+
|
102
|
+
#### `Rotoscope::trace(dest, blacklist: [], flatten: false)`
|
103
|
+
|
104
|
+
Writes all calls and returns of methods to `dest`, except for those whose filepath contains any entry in `blacklist`. `dest` is either a filename or an `IO`. The `flatten` option reduces the output data to a deduplicated list of method invocations and their caller, instead of all `call` and `return` events. Methods invoked at the top of the trace will have a caller entity of `<ROOT>` and a caller method name of `<UNKNOWN>`.
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
Rotoscope.trace(dest) { |rs| ... }
|
108
|
+
# or...
|
109
|
+
Rotoscope.trace(dest, blacklist: ["/.gem/"], flatten: true) { |rs| ... }
|
110
|
+
```
|
111
|
+
|
112
|
+
#### `Rotoscope::new(dest, blacklist: [], flatten: false)`
|
113
|
+
|
114
|
+
Same interface as `Rotoscope::trace`, but returns a `Rotoscope` instance, allowing fine-grain control via `Rotoscope#start_trace` and `Rotoscope#stop_trace`.
|
115
|
+
```ruby
|
116
|
+
rs = Rotoscope.new(dest)
|
117
|
+
# or...
|
118
|
+
rs = Rotoscope.new(dest, blacklist: ["/.gem/"], flatten: true)
|
119
|
+
```
|
120
|
+
|
121
|
+
---
|
122
|
+
|
123
|
+
### Public Instance Methods
|
124
|
+
|
125
|
+
#### `Rotoscope#trace(&block)`
|
126
|
+
|
127
|
+
Similar to `Rotoscope::trace`, but does not need to create a file handle on invocation.
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
rs = Rotoscope.new(dest)
|
131
|
+
rs.trace do |rotoscope|
|
132
|
+
# code to trace...
|
133
|
+
end
|
134
|
+
```
|
135
|
+
|
136
|
+
#### `Rotoscope#start_trace`
|
137
|
+
|
138
|
+
Begins writing method calls and returns to the `dest` specified in the initializer.
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
rs = Rotoscope.new(dest)
|
142
|
+
rs.start_trace
|
143
|
+
# code to trace...
|
144
|
+
rs.stop_trace
|
145
|
+
```
|
146
|
+
|
147
|
+
#### `Rotoscope#stop_trace`
|
148
|
+
|
149
|
+
Stops writing method invocations to the `dest`. Subsequent calls to `Rotoscope#start_trace` may be invoked to resume tracing.
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
rs = Rotoscope.new(dest)
|
153
|
+
rs.start_trace
|
154
|
+
# code to trace...
|
155
|
+
rs.stop_trace
|
156
|
+
```
|
157
|
+
|
158
|
+
#### `Rotoscope#mark(str = "")`
|
159
|
+
|
160
|
+
Inserts a marker '--- ' to divide output. Useful for segmenting multiple blocks of code that are being profiled. If `str` is provided, the line will be prefixed by '--- ', followed by the string passed.
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
rs = Rotoscope.new(dest)
|
164
|
+
rs.start_trace
|
165
|
+
# code to trace...
|
166
|
+
rs.mark('Something goes wrong here') # produces `--- Something goes wrong here` in the output
|
167
|
+
# more code ...
|
168
|
+
rs.stop_trace
|
169
|
+
```
|
170
|
+
|
171
|
+
#### `Rotoscope#close`
|
172
|
+
|
173
|
+
Flushes the buffer and closes the file handle. Once this is invoked, no more writes can be performed on the `Rotoscope` object. Sets `state` to `:closed`.
|
174
|
+
|
175
|
+
```ruby
|
176
|
+
rs = Rotoscope.new(dest)
|
177
|
+
rs.trace { |rotoscope| ... }
|
178
|
+
rs.close
|
179
|
+
```
|
180
|
+
|
181
|
+
#### `Rotoscope#state`
|
182
|
+
|
183
|
+
Returns the current state of the Rotoscope object. Valid values are `:open`, `:tracing` and `:closed`.
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
rs = Rotoscope.new(dest)
|
187
|
+
rs.state # :open
|
188
|
+
rs.trace do
|
189
|
+
rs.state # :tracing
|
190
|
+
end
|
191
|
+
rs.close
|
192
|
+
rs.state # :closed
|
193
|
+
```
|
194
|
+
|
195
|
+
#### `Rotoscope#closed?`
|
196
|
+
|
197
|
+
Shorthand to check if the `state` is set to `:closed`.
|
198
|
+
|
199
|
+
```ruby
|
200
|
+
rs = Rotoscope.new(dest)
|
201
|
+
rs.closed? # false
|
202
|
+
rs.close
|
203
|
+
rs.closed? # true
|
204
|
+
```
|
205
|
+
|
206
|
+
|
207
|
+
#### `Rotoscope#log_path`
|
208
|
+
|
209
|
+
Returns the output filepath set in the Rotoscope constructor.
|
210
|
+
|
211
|
+
```ruby
|
212
|
+
dest = '/foo/bar/rotoscope.csv'
|
213
|
+
rs = Rotoscope.new(dest)
|
214
|
+
rs.log_path # "/foo/bar/rotoscope.csv"
|
215
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# ==========================================================
|
3
|
+
# Packaging
|
4
|
+
# ==========================================================
|
5
|
+
GEMSPEC = Gem::Specification.load('rotoscope.gemspec')
|
6
|
+
|
7
|
+
require 'rubygems/package_task'
|
8
|
+
Gem::PackageTask.new(GEMSPEC) do |pkg|
|
9
|
+
end
|
10
|
+
|
11
|
+
# ==========================================================
|
12
|
+
# Ruby Extension
|
13
|
+
# ==========================================================
|
14
|
+
|
15
|
+
require 'rake/extensiontask'
|
16
|
+
Rake::ExtensionTask.new('rotoscope', GEMSPEC) do |ext|
|
17
|
+
ext.lib_dir = 'lib/rotoscope'
|
18
|
+
end
|
19
|
+
|
20
|
+
task build: :compile
|
21
|
+
|
22
|
+
task install: [:build] do |_t|
|
23
|
+
sh "gem build rotoscope.gemspec && gem install rotoscope-*.gem"
|
24
|
+
end
|
25
|
+
|
26
|
+
# ==========================================================
|
27
|
+
# Testing
|
28
|
+
# ==========================================================
|
29
|
+
|
30
|
+
require 'rake/testtask'
|
31
|
+
Rake::TestTask.new 'test' do |t|
|
32
|
+
t.test_files = FileList['test/*_test.rb']
|
33
|
+
end
|
34
|
+
task test: :build
|
35
|
+
|
36
|
+
task default: :test
|
data/bin/fmt
ADDED
data/dev.yml
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
#include "callsite.h"
|
2
|
+
#include <ruby.h>
|
3
|
+
#include <ruby/debug.h>
|
4
|
+
|
5
|
+
VALUE empty_ruby_string;
|
6
|
+
|
7
|
+
// Need the cfp field from this internal ruby structure.
|
8
|
+
struct rb_trace_arg_struct {
|
9
|
+
// unused fields needed to make sure the cfp is at the
|
10
|
+
// correct offset
|
11
|
+
rb_event_flag_t unused1;
|
12
|
+
void *unused2;
|
13
|
+
void *cfp;
|
14
|
+
// rest of fields are unused
|
15
|
+
};
|
16
|
+
|
17
|
+
size_t ruby_control_frame_size;
|
18
|
+
|
19
|
+
// We depend on MRI to store ruby control frames as an array
|
20
|
+
// to determine the control frame size, which is used here to
|
21
|
+
// get the caller's control frame
|
22
|
+
static void *caller_cfp(void *cfp) {
|
23
|
+
return ((char *)cfp) + ruby_control_frame_size;
|
24
|
+
}
|
25
|
+
|
26
|
+
static VALUE dummy(VALUE self, VALUE first) {
|
27
|
+
if (first == Qtrue) {
|
28
|
+
rb_funcall(self, rb_intern("dummy"), 1, Qfalse);
|
29
|
+
}
|
30
|
+
return Qnil;
|
31
|
+
}
|
32
|
+
|
33
|
+
static void trace_control_frame_size(VALUE tpval, void *data) {
|
34
|
+
void **cfps = data;
|
35
|
+
rb_trace_arg_t *trace_arg = rb_tracearg_from_tracepoint(tpval);
|
36
|
+
|
37
|
+
if (cfps[0] == NULL) {
|
38
|
+
cfps[0] = trace_arg->cfp;
|
39
|
+
} else if (cfps[1] == NULL) {
|
40
|
+
cfps[1] = trace_arg->cfp;
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
rs_callsite_t c_callsite(rb_trace_arg_t *trace_arg) {
|
45
|
+
VALUE path = rb_tracearg_path(trace_arg);
|
46
|
+
return (rs_callsite_t){
|
47
|
+
.filepath = NIL_P(path) ? empty_ruby_string : path,
|
48
|
+
.lineno = FIX2INT(rb_tracearg_lineno(trace_arg)),
|
49
|
+
};
|
50
|
+
}
|
51
|
+
|
52
|
+
rs_callsite_t ruby_callsite(rb_trace_arg_t *trace_arg) {
|
53
|
+
void *old_cfp = trace_arg->cfp;
|
54
|
+
|
55
|
+
// Ruby uses trace_arg->cfp to get the path and line number
|
56
|
+
trace_arg->cfp = caller_cfp(trace_arg->cfp);
|
57
|
+
rs_callsite_t callsite = c_callsite(trace_arg);
|
58
|
+
trace_arg->cfp = old_cfp;
|
59
|
+
|
60
|
+
return callsite;
|
61
|
+
}
|
62
|
+
|
63
|
+
void init_callsite() {
|
64
|
+
empty_ruby_string = rb_str_new_literal("");
|
65
|
+
RB_OBJ_FREEZE(empty_ruby_string);
|
66
|
+
rb_global_variable(&empty_ruby_string);
|
67
|
+
|
68
|
+
VALUE tmp_obj = rb_funcall(rb_cObject, rb_intern("new"), 0);
|
69
|
+
rb_define_singleton_method(tmp_obj, "dummy", dummy, 1);
|
70
|
+
|
71
|
+
char *cfps[2] = {NULL, NULL};
|
72
|
+
VALUE tracepoint = rb_tracepoint_new(Qnil, RUBY_EVENT_C_CALL,
|
73
|
+
trace_control_frame_size, &cfps);
|
74
|
+
rb_tracepoint_enable(tracepoint);
|
75
|
+
rb_funcall(tmp_obj, rb_intern("dummy"), 1, Qtrue);
|
76
|
+
rb_tracepoint_disable(tracepoint);
|
77
|
+
ruby_control_frame_size = (size_t)cfps[0] - (size_t)cfps[1];
|
78
|
+
}
|
@@ -0,0 +1,17 @@
|
|
1
|
+
#ifndef _INC_CALLSITE_H_
|
2
|
+
#define _INC_CALLSITE_H_
|
3
|
+
|
4
|
+
#include <ruby.h>
|
5
|
+
#include <ruby/debug.h>
|
6
|
+
|
7
|
+
typedef struct {
|
8
|
+
VALUE filepath;
|
9
|
+
unsigned int lineno;
|
10
|
+
} rs_callsite_t;
|
11
|
+
|
12
|
+
void init_callsite();
|
13
|
+
|
14
|
+
rs_callsite_t c_callsite(rb_trace_arg_t *trace_arg);
|
15
|
+
rs_callsite_t ruby_callsite(rb_trace_arg_t *trace_arg);
|
16
|
+
|
17
|
+
#endif
|