rotoscope 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,2 @@
1
+ ---
2
+ BasedOnStyle: Google
@@ -0,0 +1,3 @@
1
+ Make sure these are checked before submitting your PR — thank you!
2
+
3
+ - [ ] `bin/fmt` was successfully run
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ tmp
2
+ lib/rotoscope/rotoscope.bundle
3
+ lib/rotoscope/rotoscope.so
4
+ .vscode
5
+ *.gem
6
+ .rubocop-*
7
+ Gemfile.lock
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
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+ source 'https://rubygems.org'
3
+ gemspec
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
@@ -0,0 +1,9 @@
1
+ #! /usr/bin/env bash
2
+
3
+ echo "Formatting C code..."
4
+ find "$PWD" -iname lib/rotoscope/*.h -o -iname *.c | xargs clang-format -i -style=file
5
+
6
+ echo "Formatting Ruby code..."
7
+ bundle exec rubocop -a
8
+
9
+ echo "Formatting complete."
data/dev.yml ADDED
@@ -0,0 +1,12 @@
1
+ ---
2
+ name: rotoscope
3
+ up:
4
+ - ruby: 2.3.3
5
+ - homebrew:
6
+ - clang-format
7
+ - bundler
8
+
9
+ commands:
10
+ build: bundle exec rake build
11
+ test: bundle exec rake test
12
+ fmt: bin/fmt
@@ -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