exec_trace 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cbb1b6cf45d5283b3c16f4084bfaca1578453758f34e5fbfc178a4d4b6ce0abb
4
+ data.tar.gz: 2b66700283869ccf821b851b4e00d1c76043789e50a9d73de9f7698bfa05a417
5
+ SHA512:
6
+ metadata.gz: ec2102be13afe2de8409f89348b5e4d032344315a1ecb996c19de5e75873f16f2994abd9e2e2e6e6e3f2a1970923f2c90a26dfe6aa82a8210f844c9de36a8bf7
7
+ data.tar.gz: c72678f58fa819a54d92660e756cc108fa7255f3dbf4d297cd9a45fe4710b03e69a95c7faae9b4936fdec07b48b4c8c60364f9f798f3b9987e4d56fdc8dffba6
@@ -0,0 +1,19 @@
1
+ name: Ruby
2
+
3
+ on: [push,pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v2
10
+ - name: Set up Ruby
11
+ uses: ruby/setup-ruby@v1
12
+ with:
13
+ ruby-version: 2.7.1
14
+ - name: Run the default task
15
+ run: |
16
+ gem install bundler -v 2.2.9
17
+ bundle install
18
+ bundle exec rake compile
19
+ bundle exec rake
data/.gitignore ADDED
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ ext/Makefile
11
+ ext/extconf.h
12
+ ext/mkmf.log
13
+ lib
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in exec_trace.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "minitest", "~> 5.0"
data/Gemfile.lock ADDED
@@ -0,0 +1,24 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ exec_trace (0.0.1)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ minitest (5.14.3)
10
+ rake (13.0.3)
11
+ rake-compiler (1.1.1)
12
+ rake
13
+
14
+ PLATFORMS
15
+ x86_64-darwin-19
16
+
17
+ DEPENDENCIES
18
+ exec_trace!
19
+ minitest (~> 5.0)
20
+ rake (~> 13.0)
21
+ rake-compiler (~> 1.1)
22
+
23
+ BUNDLED WITH
24
+ 2.2.9
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Blake Williams
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,49 @@
1
+ # ExecTrace
2
+
3
+ Trace Ruby code, returning methods that were run, how many times they were
4
+ called, how long they took to run, and the methods they called.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'exec_trace'
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ```ruby
17
+ require "pp"
18
+ require "exec_trace"
19
+
20
+ result = exec_trace do
21
+ u = User.new
22
+ u.save!
23
+ end
24
+
25
+ pp result
26
+ ```
27
+
28
+ `exec_trace` returns an array of arrays. Each top-level array is a top-level call in the
29
+ `exec_trace` block. Frame consist 4 fields: file name + line number,
30
+ calls, time in microseconds, and an array of frames that it called.
31
+
32
+ e.g.
33
+
34
+ ```
35
+ [
36
+ ["/Users/me/exec_trace/test/exec_trace_test.rb:23", 1, 52, [
37
+ ["/Users/me/exec_trace/test/exec_trace_test.rb:24", 5, 2518388, []]
38
+ ]]
39
+ ]
40
+ ```
41
+
42
+ ## Developing
43
+
44
+ * To compile the C extension: `bundle exec rake compile`
45
+ * To run the tests `bundle exec rake test`
46
+
47
+ ## License
48
+
49
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ task default: :test
13
+
14
+ # Extension
15
+ require "rake/extensiontask"
16
+
17
+ Rake::ExtensionTask.new("exec_trace") do |e|
18
+ e.ext_dir = "ext"
19
+ end
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "exec_trace"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "exec_trace"
5
+ spec.version = "0.0.1"
6
+ spec.authors = ["Blake Williams"]
7
+ spec.email = ["blake@blakewilliams.me"]
8
+
9
+ spec.summary = "Trace your Ruby function's execution path"
10
+ spec.description = spec.summary
11
+ spec.homepage = "https://github.com/blakewilliams/exec_trace"
12
+ spec.license = "MIT"
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")
14
+
15
+ spec.metadata["homepage_uri"] = spec.homepage
16
+
17
+ # Specify which files should be added to the gem when it is released.
18
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
20
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
21
+ end
22
+ spec.bindir = "exe"
23
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
24
+ spec.require_paths = ["lib"]
25
+
26
+ spec.extensions = ["ext/extconf.rb"]
27
+
28
+ spec.add_development_dependency "rake-compiler", "~> 1.1"
29
+ end
data/ext/exec_trace.c ADDED
@@ -0,0 +1,267 @@
1
+ #include <ruby.h>
2
+ #include <stdio.h>
3
+
4
+ #include <sys/time.h>
5
+ #include <time.h>
6
+
7
+ #include <ruby/debug.h>
8
+ #include <ruby/intern.h>
9
+
10
+ typedef uint64_t usec_t;
11
+
12
+ static usec_t
13
+ wall_usec()
14
+ {
15
+ struct timeval tv;
16
+ gettimeofday(&tv, NULL);
17
+
18
+ // Convert timestamp to us
19
+ return (usec_t)tv.tv_sec * 1000000 + (usec_t)tv.tv_usec;
20
+ }
21
+
22
+ typedef struct frame
23
+ {
24
+ rb_event_flag_t event;
25
+ char* file_name;
26
+ int line_number;
27
+ int calls;
28
+
29
+ struct frame* parent;
30
+
31
+ int subframe_count;
32
+ int subframe_size;
33
+ struct frame** subframes;
34
+
35
+ usec_t wall_total_usec;
36
+ usec_t wall_start_usec;
37
+ usec_t wall_children_usec;
38
+ } frame_t;
39
+
40
+ static struct
41
+ {
42
+ uint64_t top_frame_used;
43
+ frame_t* top_frames[65536];
44
+ frame_t* last_frame;
45
+ VALUE thread;
46
+ } tracer = {};
47
+
48
+ static void
49
+ trace_hook(rb_event_flag_t event, VALUE data, VALUE self, ID mid, VALUE klass)
50
+ {
51
+ // Ignore other threads
52
+ if (tracer.thread != rb_thread_current()) {
53
+ return;
54
+ }
55
+
56
+ int buff_size = 2;
57
+ VALUE buff[buff_size];
58
+ int lines[buff_size];
59
+ // https://github.com/ruby/ruby/blob/48b94b791997881929c739c64f95ac30f3fd0bb9/include/ruby/debug.h
60
+ // start 0, limit to 2 frames, buff = iseqs, lines == line numbers
61
+ int collected_size = rb_profile_frames(0, buff_size, buff, lines);
62
+
63
+ if (collected_size == 0) {
64
+ return;
65
+ }
66
+
67
+ int line_index = 0;
68
+
69
+ if (!mid && collected_size == 2) {
70
+ line_index = 1;
71
+ }
72
+
73
+ VALUE path = rb_profile_frame_absolute_path(buff[line_index]);
74
+
75
+ if (NIL_P(path)) {
76
+ line_index = 0;
77
+ path = rb_profile_frame_absolute_path(buff[line_index]);
78
+ if (NIL_P(path)) {
79
+ return;
80
+ }
81
+ }
82
+ char* file = StringValueCStr(path);
83
+ int line = lines[line_index];
84
+
85
+ if (!file || line <= 0) {
86
+ return;
87
+ }
88
+
89
+ frame_t* frame;
90
+ frame_t* last_top_frame = tracer.top_frames[tracer.top_frame_used - 1];
91
+
92
+ switch (event) {
93
+ case RUBY_EVENT_CALL:
94
+ case RUBY_EVENT_C_CALL:
95
+ if (tracer.last_frame && tracer.last_frame->file_name == file &&
96
+ tracer.last_frame->line_number == line) {
97
+ // collapse duplicate frames
98
+ return;
99
+ } else if (last_top_frame && last_top_frame->file_name == file &&
100
+ last_top_frame->line_number == line) {
101
+ // collapse duplicate top-level frames
102
+ return;
103
+ } else {
104
+ if (tracer.last_frame) {
105
+ for (int i = 0; i < tracer.last_frame->subframe_count; i++) {
106
+ frame = tracer.last_frame->subframes[i];
107
+
108
+ if (frame->file_name == file && frame->line_number == line) {
109
+ frame->calls++;
110
+ frame->wall_start_usec = wall_usec();
111
+ tracer.last_frame = frame;
112
+ return;
113
+ }
114
+ }
115
+ }
116
+
117
+ frame = malloc(sizeof(frame_t));
118
+
119
+ frame->event = event;
120
+ frame->file_name = file;
121
+ frame->line_number = line;
122
+ frame->calls = 1;
123
+ frame->subframe_count = 0;
124
+ frame->subframe_size = 1;
125
+ frame->subframes = malloc(1 * sizeof(frame_t*));
126
+ frame->wall_start_usec = wall_usec();
127
+ frame->wall_total_usec = 0;
128
+ frame->wall_children_usec = 0;
129
+ frame->parent = tracer.last_frame;
130
+
131
+ if (tracer.last_frame) {
132
+ frame_t* last_frame = tracer.last_frame;
133
+ if (last_frame->subframe_size == last_frame->subframe_count) {
134
+ last_frame->subframe_size *= 2;
135
+ tracer.last_frame->subframes =
136
+ realloc(tracer.last_frame->subframes,
137
+ sizeof(frame_t*) * last_frame->subframe_size);
138
+ }
139
+
140
+ last_frame->subframe_count++;
141
+ last_frame->subframes[last_frame->subframe_count - 1] = frame;
142
+ } else {
143
+ tracer.top_frame_used++;
144
+ tracer.top_frames[tracer.top_frame_used - 1] = frame;
145
+ }
146
+ }
147
+
148
+ tracer.last_frame = frame;
149
+ break;
150
+ case RUBY_EVENT_RETURN:
151
+ case RUBY_EVENT_C_RETURN: {
152
+ frame_t* current_frame = tracer.last_frame;
153
+ while (1) {
154
+ if (current_frame && current_frame->file_name == file &&
155
+ current_frame->line_number == line) {
156
+ usec_t time = wall_usec() - tracer.last_frame->wall_start_usec;
157
+ if (tracer.last_frame->parent) {
158
+ tracer.last_frame->parent->wall_children_usec += time;
159
+ }
160
+ tracer.last_frame->wall_total_usec += time;
161
+ tracer.last_frame = current_frame->parent;
162
+ return;
163
+ } else if (current_frame) {
164
+ current_frame = current_frame->parent;
165
+ } else {
166
+ return;
167
+ }
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ /*
174
+ Ensure trace is cleaned up by removing the event hook.
175
+ */
176
+ static VALUE
177
+ exec_trace_ensure(VALUE self)
178
+ {
179
+ rb_remove_event_hook((rb_event_hook_func_t)trace_hook);
180
+
181
+ return self;
182
+ }
183
+
184
+ static void
185
+ create_frame_array(VALUE arr, frame_t* frame)
186
+ {
187
+ VALUE fname = rb_sprintf("%s:%i", frame->file_name, frame->line_number);
188
+ rb_ary_push(arr, fname);
189
+ rb_ary_push(arr, INT2FIX(frame->calls));
190
+ rb_ary_push(arr, INT2FIX(frame->wall_total_usec - frame->wall_children_usec));
191
+
192
+ VALUE subframe_ary = rb_ary_new();
193
+
194
+ if (frame->subframe_count == 0) {
195
+ rb_ary_push(arr, subframe_ary);
196
+ return;
197
+ }
198
+
199
+ for (int i = 0; i < frame->subframe_count; i++) {
200
+ VALUE frame_ary = rb_ary_new();
201
+ create_frame_array(frame_ary, frame->subframes[i]);
202
+ rb_ary_push(subframe_ary, frame_ary);
203
+ }
204
+
205
+ rb_ary_push(arr, subframe_ary);
206
+ }
207
+
208
+ static void
209
+ cleanup(frame_t* frame)
210
+ {
211
+ if (frame->subframe_count > 0) {
212
+ for (int i = 0; i < frame->subframe_count; i++) {
213
+ cleanup(frame->subframes[i]);
214
+ }
215
+ }
216
+
217
+ free(frame->subframes);
218
+ free(frame);
219
+ }
220
+
221
+ /*
222
+ Global function that begins tracing
223
+ */
224
+ VALUE
225
+ exec_trace(VALUE self)
226
+ {
227
+ // Capture current thread ID to ensure we don't capture frames from other
228
+ // threads.
229
+ tracer.thread = rb_thread_current();
230
+
231
+ // Start listening for call events
232
+ rb_add_event_hook((rb_event_hook_func_t)trace_hook,
233
+ RUBY_EVENT_CALL | RUBY_EVENT_C_CALL | RUBY_EVENT_RETURN |
234
+ RUBY_EVENT_C_RETURN,
235
+ Qnil);
236
+
237
+ // Call passed in block, ensure trace hook is removed
238
+ rb_ensure(rb_yield, Qnil, exec_trace_ensure, self);
239
+
240
+ VALUE top_level_frame_ary = rb_ary_new();
241
+
242
+ for (uint64_t i = 0; i < tracer.top_frame_used; i++) {
243
+ VALUE frame_ary = rb_ary_new();
244
+ create_frame_array(frame_ary, tracer.top_frames[i]);
245
+ rb_ary_push(top_level_frame_ary, frame_ary);
246
+
247
+ // Cleanup malloc'd memory
248
+ cleanup(tracer.top_frames[i]);
249
+ }
250
+
251
+ // Cleanup for another run
252
+ tracer.top_frame_used = 0;
253
+ tracer.last_frame = NULL;
254
+ memset(tracer.top_frames, 0, sizeof(tracer.top_frames));
255
+
256
+ return top_level_frame_ary;
257
+ }
258
+
259
+ /*
260
+ Entrypoint for a gem extension and defines the global
261
+ `exec_trace` function.
262
+ */
263
+ void
264
+ Init_exec_trace(void)
265
+ {
266
+ rb_define_global_function("exec_trace", exec_trace, 0);
267
+ }
data/ext/extconf.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'mkmf'
2
+
3
+ have_header "ruby/intern.h"
4
+ have_header "ruby/debug.h"
5
+ have_func('rb_profile_frames')
6
+ have_func('rb_thread_current')
7
+
8
+ create_makefile "exec_trace"
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: exec_trace
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Blake Williams
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-02-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake-compiler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.1'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.1'
27
+ description: Trace your Ruby function's execution path
28
+ email:
29
+ - blake@blakewilliams.me
30
+ executables: []
31
+ extensions:
32
+ - ext/extconf.rb
33
+ extra_rdoc_files: []
34
+ files:
35
+ - ".github/workflows/main.yml"
36
+ - ".gitignore"
37
+ - Gemfile
38
+ - Gemfile.lock
39
+ - LICENSE.txt
40
+ - README.md
41
+ - Rakefile
42
+ - bin/console
43
+ - bin/setup
44
+ - exec_trace.gemspec
45
+ - ext/exec_trace.c
46
+ - ext/extconf.rb
47
+ homepage: https://github.com/blakewilliams/exec_trace
48
+ licenses:
49
+ - MIT
50
+ metadata:
51
+ homepage_uri: https://github.com/blakewilliams/exec_trace
52
+ post_install_message:
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 2.4.0
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubygems_version: 3.1.2
68
+ signing_key:
69
+ specification_version: 4
70
+ summary: Trace your Ruby function's execution path
71
+ test_files: []