tapping_device 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 31bb339b2993e3656c953c0c403b4145d3047722919f86ab4d0683c577379027
4
- data.tar.gz: 76b0b6fab4f3998173960d3a0bce5bdc6faa8155bc49fe4ec3a0ecd0196e8feb
3
+ metadata.gz: 4000e62db2d9672d87d133a5c6adeeaf5eed9d134b4bd8b9a948887ee2d06f48
4
+ data.tar.gz: 477c9bf04c5a218aa8a23034929bcae7d0d52e663954380bfeb7b57f1290152c
5
5
  SHA512:
6
- metadata.gz: 7641adbd68125ce2cb1a2fdb96969d09dcedc8d35b0c24fc1e0136badaca586bf7287631c3b6fecc7d3909958e0c49bd2d05d2de3b41845f680fb19165ccab1e
7
- data.tar.gz: f9d685094c3f45b3e330c2725fc61bc01a9ae11cb5e486a1fec5de9a2131860cd64160e1d83753210529dc3383523d8072437e8ff285fc589b6bb6aafe5fcf5c
6
+ metadata.gz: 629ce1239a852d1bc1e9b7585ad7746273a5722139a08b24a8712448af9fc3201915eb3d882d7eed5300afaba590ee880e3db103a9fa9825d5304613e278ad23
7
+ data.tar.gz: f03e06164e74ceab8bbef4f0dec75795fab1866ba427dbcc570a4dac6bdf5a7ac3c166e0d4a2af7a6b042d1fd74cc9ad7465c960254fe6536258bd7fa1b81031
data/Gemfile.lock CHANGED
@@ -1,30 +1,31 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- tapping_device (0.1.0)
5
- activerecord (~> 6.0)
4
+ tapping_device (0.1.1)
5
+ activerecord (~> 5.2)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- activemodel (6.0.0)
11
- activesupport (= 6.0.0)
12
- activerecord (6.0.0)
13
- activemodel (= 6.0.0)
14
- activesupport (= 6.0.0)
15
- activesupport (6.0.0)
10
+ activemodel (5.2.3)
11
+ activesupport (= 5.2.3)
12
+ activerecord (5.2.3)
13
+ activemodel (= 5.2.3)
14
+ activesupport (= 5.2.3)
15
+ arel (>= 9.0)
16
+ activesupport (5.2.3)
16
17
  concurrent-ruby (~> 1.0, >= 1.0.2)
17
18
  i18n (>= 0.7, < 2)
18
19
  minitest (~> 5.1)
19
20
  tzinfo (~> 1.1)
20
- zeitwerk (~> 2.1, >= 2.1.8)
21
+ arel (9.0.0)
21
22
  coderay (1.1.2)
22
23
  concurrent-ruby (1.1.5)
23
24
  diff-lcs (1.3)
24
25
  i18n (1.7.0)
25
26
  concurrent-ruby (~> 1.0)
26
27
  method_source (0.9.2)
27
- minitest (5.12.2)
28
+ minitest (5.13.0)
28
29
  pry (0.12.2)
29
30
  coderay (~> 1.1.0)
30
31
  method_source (~> 0.9.0)
@@ -42,11 +43,10 @@ GEM
42
43
  diff-lcs (>= 1.2.0, < 2.0)
43
44
  rspec-support (~> 3.8.0)
44
45
  rspec-support (3.8.2)
45
- sqlite3 (1.4.1)
46
+ sqlite3 (1.3.13)
46
47
  thread_safe (0.3.6)
47
48
  tzinfo (1.2.5)
48
49
  thread_safe (~> 0.1)
49
- zeitwerk (2.2.0)
50
50
 
51
51
  PLATFORMS
52
52
  ruby
@@ -56,7 +56,7 @@ DEPENDENCIES
56
56
  pry
57
57
  rake (~> 10.0)
58
58
  rspec (~> 3.0)
59
- sqlite3 (~> 1.4.1)
59
+ sqlite3 (~> 1.3.6)
60
60
  tapping_device!
61
61
 
62
62
  BUNDLED WITH
data/README.md CHANGED
@@ -6,13 +6,13 @@
6
6
 
7
7
  ```ruby
8
8
  class PostsController < ApplicationController
9
- include TappingDevice::Trackable
10
-
11
9
  def show
12
10
  @post = Post.find(params[:id])
13
- tap_on!(@post) do |payload|
11
+
12
+ device = TappingDevice.new do |payload|
14
13
  puts "Method: #{payload[:method_name]} line: #{payload[:filepath]}:#{payload[:line_number]}"
15
14
  end
15
+ device.tap_on!(@post)
16
16
  end
17
17
  end
18
18
  ```
@@ -28,9 +28,10 @@ Method: to_param line: /RUBY_PATH/gems/2.6.0/gems/actionpack-5.2.0/lib/action_di
28
28
  Or you can use `tap_assoc!`. This is very useful for tracking potential n+1 query calls, here’s a sample from my work
29
29
 
30
30
  ```ruby
31
- tap_assoc!(order) do |payload|
31
+ device = TappingDevice.new do |payload|
32
32
  puts "Assoc: #{payload[:method_name]} line: #{payload[:filepath]}:#{payload[:line_number]}"
33
33
  end
34
+ device.tap_assoc!(order)
34
35
  ```
35
36
 
36
37
  ```
@@ -65,15 +66,44 @@ $ gem install tapping_device
65
66
  ```
66
67
 
67
68
  ## Usage
68
- In order to use `tapping_device`, you need to include `TappingDevice::Trackable` module in where you want to track your code.
69
69
 
70
- ### Methods
71
- - `tap_init!(class)` - tracks a class’ instance initialization
72
- - `tap_on!(object)` - tracks any calls received by the object
73
- - `tap_assoc!(activerecord_object)` - tracks association calls on a record, like `post.comments`
74
- - `untap!(object)` - this stops tapping on the given object
70
+ ### Create a device object
71
+ In order to tap on something, you need to first initialize a tapping device with a block that process the call info.
72
+
73
+ ```ruby
74
+ device = TappingDevice.new do |payload|
75
+ if payload[:method_name].to_s.match?(/foo/)
76
+ puts "Method: #{payload[:method_name]} line: #{payload[:filepath]}:#{payload[:line_number]}"
77
+ end
78
+ end
79
+ ```
80
+
81
+ ### Performance issue and setup stop condition
82
+
83
+ Because `tapping_device` is built upon `TracePoint`, which literally scans **every method call** on **every object**. Even if we filter out the calls we’re not interested in, just filtering out through those method calls takes time if your application isn’t a small one. So it’s very important to stop the tapping device at a certain point. You can do this in 2 ways:
84
+
85
+ #### Use `device.stop_when(&block)` to set a stop condition
86
+ To define a stop condition, you can use `stop_when` method.
75
87
 
76
- ### Info of the call
88
+ ```ruby
89
+ device.stop_when do |payload|
90
+ device.calls.count >= 10 # stop after gathering 10 calls' data
91
+ end
92
+ ```
93
+
94
+ **If you don’t set a stop condition, you need to use tapping methods that has exclamation mark**, like `device.tap_on!(post)`.
95
+
96
+ #### `device.stop!`
97
+ If you don’t define a stop condition, you can also use `device.stop!` to stop it manually.
98
+
99
+ ### Start tapping
100
+
101
+ #### Methods
102
+ - `TappingDevice#tap_init(class)` - tracks a class’ instance initialization
103
+ - `TappingDevice#tap_on(object)` - tracks any calls received by the object
104
+ - `TappingDevice#tap_assoc(activerecord_object)` - tracks association calls on a record, like `post.comments`
105
+
106
+ #### Info of the call
77
107
  All tapping methods (start with `tap_`) takes a block and yield a hash as block argument.
78
108
 
79
109
  ```ruby
@@ -110,34 +140,14 @@ The hash contains
110
140
  - `exclude_by_paths: [/path/]` - an array of call path patterns that we want to skip. This could be very helpful when working on large project like Rails applications.
111
141
  - `filter_by_paths: [/path/]` - only contain calls from the specified paths
112
142
 
113
- ```ruby
114
- tap_on!(@post, exclude_by_paths: [/active_record/]) do |payload|
115
- puts "Method: #{payload[:method_name]} line: #{payload[:filepath]}:#{payload[:line_number]}"
116
- end
117
- ```
118
-
119
- ```
120
- Method: _read_attribute line: /RUBY_PATH/gems/2.6.0/gems/activerecord-5.2.0/lib/active_record/attribute_methods/read.rb:40
121
- Method: name line: /PROJECT_PATH/sample/app/views/posts/show.html.erb:5
122
- Method: _read_attribute line: /RUBY_PATH/gems/2.6.0/gems/activerecord-5.2.0/lib/active_record/attribute_methods/read.rb:40
123
- Method: user_id line: /PROJECT_PATH/sample/app/views/posts/show.html.erb:10
124
- .......
125
-
126
- # versus
127
-
128
- Method: name line: /PROJECT_PATH/sample/app/views/posts/show.html.erb:5
129
- Method: user_id line: /PROJECT_PATH/sample/app/views/posts/show.html.erb:10
130
- Method: to_param line: /RUBY_PATH/gems/2.6.0/gems/actionpack-5.2.0/lib/action_dispatch/routing/route_set.rb:236
131
- ```
132
-
133
-
134
- ### `#tap_init!`
143
+ ### `#tap_init`
135
144
 
136
145
  ```ruby
137
146
  calls = []
138
- tap_init!(Student) do |payload|
147
+ device = TappingDevice.new do |payload|
139
148
  calls << [payload[:method_name], payload[:arguments]]
140
149
  end
150
+ device.tap_init!(Student)
141
151
 
142
152
  Student.new("Stan", 18)
143
153
  Student.new("Jane", 23)
@@ -149,16 +159,16 @@ puts(calls.to_s) #=> [[:initialize, [[:name, "Stan"], [:age, 18]]], [:initialize
149
159
 
150
160
  ```ruby
151
161
  class PostsController < ApplicationController
152
- include TappingDevice::Trackable
153
-
154
162
  before_action :set_post, only: [:show, :edit, :update, :destroy]
155
163
 
156
164
  # GET /posts/1
157
165
  # GET /posts/1.json
158
166
  def show
159
- tap_on!(@post) do |payload|
167
+ device = TappingDevice.new do |payload|
160
168
  puts "Method: #{payload[:method_name]} line: #{payload[:filepath]}:#{payload[:line_number]}"
161
169
  end
170
+
171
+ device.tap_on!(@post)
162
172
  end
163
173
  end
164
174
  ```
@@ -174,9 +184,10 @@ Method: to_param line: /RUBY_PATH/gems/2.6.0/gems/actionpack-5.2.0/lib/action_di
174
184
  ### `tap_assoc!`
175
185
 
176
186
  ```ruby
177
- tap_assoc!(order) do |payload|
187
+ device = TappingDevice.new do |payload|
178
188
  puts "Assoc: #{payload[:method_name]} line: #{payload[:filepath]}:#{payload[:line_number]}"
179
189
  end
190
+ device.tap_assoc!(order)
180
191
  ```
181
192
 
182
193
  ```
@@ -187,6 +198,20 @@ Assoc: amending_orders line: /MY_PROJECT/app/models/order.rb:385
187
198
  Assoc: amends_order line: /MY_PROJECT/app/models/order.rb:432
188
199
  ```
189
200
 
201
+ ### Device states & Managing Devices
202
+
203
+ Every `TappingDevice` instance can have 3 states:
204
+
205
+ - `Initial` - means the instance is initialized but hasn’t been used to tap on anything.
206
+ - `Enabled` - means the instance has started to tap on something (has called `tap_*` methods).
207
+ - `Disabled` - means the instance has been disabled. It will no longer receive any call info.
208
+
209
+ When debugging, we may create many device instances and tap objects in several places. Then it’ll be quite annoying to manage their states. So `TappingDevice` has several class methods that allows you to manage all `TappingDevice` instances:
210
+
211
+ - `TappingDevice.devices` - Lists all registered devices with `initial` or `enabled` state. Note that any instance that’s been stopped will be removed from the list.
212
+ - `TappingDevice.stop_all!` - Stops all registered devices and remove them from the `devices` list.
213
+ - `TappingDevice.suspend_new!` - Suspends any device instance from changing their state from `initatial` to `enabled`. Which means any `tap_*` calls after it will no longer work.
214
+ - `TappingDevice.reset!` - Cancels `suspend_new` (if called) and stops/removes all created devices. Useful to reset environment between test cases.
190
215
 
191
216
  ## Development
192
217
 
@@ -1,7 +1,170 @@
1
+ require "active_record"
1
2
  require "tapping_device/version"
2
3
  require "tapping_device/trackable"
4
+ require "tapping_device/exceptions"
3
5
 
4
- module TappingDevice
5
- class Error < StandardError; end
6
- # Your code goes here...
6
+ class TappingDevice
7
+ CALLER_START_POINT = 2
8
+ FORCE_STOP_WHEN_MESSAGE = "You must set stop_when condition before start tapping"
9
+
10
+ attr_reader :options, :calls, :trace_point
11
+
12
+ @@devices = []
13
+ @@suspend_new = false
14
+
15
+ # list all registered devices
16
+ def self.devices
17
+ @@devices
18
+ end
19
+
20
+ # disable given device and remove it from registered list
21
+ def self.delete_device(device)
22
+ device.trace_point&.disable
23
+ @@devices -= [device]
24
+ end
25
+
26
+ # stops all registered devices and remove them from registered list
27
+ def self.stop_all!
28
+ @@devices.each(&:stop!)
29
+ end
30
+
31
+ # suspend enabling new trace points
32
+ # user can still create new Device instances, but they won't be functional
33
+ def self.suspend_new!
34
+ @@suspend_new = true
35
+ end
36
+
37
+ # reset everything to clean state and disable all devices
38
+ def self.reset!
39
+ @@suspend_new = false
40
+ stop_all!
41
+ end
42
+
43
+ def initialize(options = {}, &block)
44
+ @block = block
45
+ @options = options
46
+ @calls = []
47
+ self.class.devices << self
48
+ end
49
+
50
+ def tap_init!(klass)
51
+ raise "argument should be a class, got #{klass}" unless klass.is_a?(Class)
52
+ track(klass, condition: :tap_init?, block: @block, **@options)
53
+ end
54
+
55
+ def tap_on!(object)
56
+ track(object, condition: :tap_on?, block: @block, **@options)
57
+ end
58
+
59
+ def tap_assoc!(record)
60
+ raise "argument should be an instance of ActiveRecord::Base" unless record.is_a?(ActiveRecord::Base)
61
+ track(record, condition: :tap_associations?, block: @block, **@options)
62
+ end
63
+
64
+ def tap_init(klass)
65
+ validate_tapping(__method__)
66
+ tap_init!(klass)
67
+ end
68
+
69
+ def tap_on(object)
70
+ validate_tapping(__method__)
71
+ tap_on!(object)
72
+ end
73
+
74
+ def tap_assoc(record)
75
+ validate_tapping(__method__)
76
+ tap_assoc!(record)
77
+ end
78
+
79
+ def set_block(&block)
80
+ @block = block
81
+ end
82
+
83
+ def stop!
84
+ self.class.delete_device(self)
85
+ end
86
+
87
+ def stop_when(&block)
88
+ @stop_when = block
89
+ end
90
+
91
+ private
92
+
93
+ def validate_tapping(method_name)
94
+ unless @stop_when
95
+ raise TappingDevice::Exception.new <<~ERROR
96
+ You must set stop_when condition before calling #{method_name}. Or you can use #{method_name}! to force tapping.
97
+ Tapping without stop condition can largely slow down or even halt your application, because it'll need to
98
+ screen literally every call happened.
99
+ ERROR
100
+ end
101
+ end
102
+
103
+ def track(object, condition:, block:, with_trace_to: nil, exclude_by_paths: [], filter_by_paths: nil)
104
+ @trace_point = TracePoint.new(:return) do |tp|
105
+ filepath, line_number = caller(CALLER_START_POINT).first.split(":")[0..1]
106
+
107
+ # this needs to be placed upfront so we can exclude noise before doing more work
108
+ next if exclude_by_paths.any? { |pattern| pattern.match?(filepath) }
109
+
110
+ if filter_by_paths
111
+ next unless filter_by_paths.any? { |pattern| pattern.match?(filepath) }
112
+ end
113
+
114
+ arguments = tp.binding.local_variables.map { |n| [n, tp.binding.local_variable_get(n)] }
115
+
116
+ yield_parameters = {
117
+ receiver: tp.self,
118
+ method_name: tp.callee_id,
119
+ arguments: arguments,
120
+ return_value: (tp.return_value rescue nil),
121
+ filepath: filepath,
122
+ line_number: line_number,
123
+ defined_class: tp.defined_class,
124
+ trace: [],
125
+ tp: tp
126
+ }
127
+
128
+ yield_parameters[:trace] = caller[CALLER_START_POINT..(CALLER_START_POINT + with_trace_to)] if with_trace_to
129
+
130
+ if send(condition, object, yield_parameters)
131
+ if @block
132
+ @calls << block.call(yield_parameters)
133
+ else
134
+ @calls << yield_parameters
135
+ end
136
+ end
137
+
138
+ stop! if @stop_when&.call(yield_parameters)
139
+ end
140
+
141
+ @trace_point.enable unless @@suspend_new
142
+
143
+ self
144
+ end
145
+
146
+ private
147
+
148
+ def tap_init?(klass, parameters)
149
+ receiver = parameters[:receiver]
150
+ method_name = parameters[:method_name]
151
+
152
+ if klass.ancestors.include?(ActiveRecord::Base)
153
+ method_name == :new && receiver.ancestors.include?(klass)
154
+ else
155
+ method_name == :initialize && receiver.is_a?(klass)
156
+ end
157
+ end
158
+
159
+ def tap_on?(object, parameters)
160
+ parameters[:receiver].object_id == object.object_id
161
+ end
162
+
163
+ def tap_associations?(object, parameters)
164
+ return false unless tap_on?(object, parameters)
165
+
166
+ model_class = object.class
167
+ associations = model_class.reflections
168
+ associations.keys.include?(parameters[:method_name].to_s)
169
+ end
7
170
  end
@@ -0,0 +1,4 @@
1
+ class TappingDevice
2
+ class Exception < StandardError
3
+ end
4
+ end
@@ -1,98 +1,15 @@
1
- require "active_record"
2
-
3
- module TappingDevice
1
+ class TappingDevice
4
2
  module Trackable
5
- TAPPING_DEVICE = :@tapping_device
6
- CALLER_START_POINT = 2
7
-
8
- def tap_initialization_of!(klass, options = {}, &block)
9
- raise "argument should be a class, got #{klass}" unless klass.is_a?(Class)
10
- track(klass, condition: :tap_init?, block: block, **options)
11
- end
12
-
13
- def tap_association_calls!(record, options = {}, &block)
14
- raise "argument should be an instance of ActiveRecord::Base" unless record.is_a?(ActiveRecord::Base)
15
- track(record, condition: :tap_associations?, block: block, **options)
16
- end
17
-
18
- def tap_calls_on!(object, options = {}, &block)
19
- track(object, condition: :tap_on?, block: block, **options)
20
- end
21
-
22
- def stop_tapping!(object)
23
- get_tapping_device(object)&.each { |tp| tp.disable }
24
- end
25
-
26
- alias :tap_init! :tap_initialization_of!
27
- alias :tap_assoc! :tap_association_calls!
28
- alias :tap_on! :tap_calls_on!
29
- alias :untap! :stop_tapping!
30
-
31
- private
32
-
33
- def track(object, condition:, block:, with_trace_to: nil, exclude_by_paths: [], filter_by_paths: nil)
34
- trace_point = TracePoint.trace(:return) do |tp|
35
- filepath, line_number = caller(CALLER_START_POINT).first.split(":")[0..1]
36
-
37
- # this needs to be placed upfront so we can exclude noise before doing more work
38
- next if exclude_by_paths.any? { |pattern| pattern.match?(filepath) }
39
-
40
- if filter_by_paths
41
- next unless filter_by_paths.any? { |pattern| pattern.match?(filepath) }
42
- end
43
-
44
- arguments = tp.binding.local_variables.map { |n| [n, tp.binding.local_variable_get(n)] }
45
-
46
- yield_parameters = {
47
- receiver: tp.self,
48
- method_name: tp.callee_id,
49
- arguments: arguments,
50
- return_value: (tp.return_value rescue nil),
51
- filepath: filepath,
52
- line_number: line_number,
53
- defined_class: tp.defined_class,
54
- trace: [],
55
- tp: tp
56
- }
57
-
58
- yield_parameters[:trace] = caller[CALLER_START_POINT..(CALLER_START_POINT + with_trace_to)] if with_trace_to
59
-
60
- block.call(yield_parameters) if send(condition, object, yield_parameters)
61
- end
62
-
63
- add_tapping_device(object, trace_point)
64
- end
65
-
66
- def tap_init?(klass, parameters)
67
- receiver = parameters[:receiver]
68
- method_name = parameters[:method_name]
69
-
70
- if klass.ancestors.include?(ActiveRecord::Base)
71
- method_name == :new && receiver.ancestors.include?(klass)
72
- else
73
- method_name == :initialize && receiver.is_a?(klass)
74
- end
75
- end
76
-
77
- def tap_on?(object, parameters)
78
- parameters[:receiver].object_id == object.object_id
79
- end
80
-
81
- def tap_associations?(object, parameters)
82
- return false unless tap_on?(object, parameters)
83
-
84
- model_class = object.class
85
- associations = model_class.reflections
86
- associations.keys.include?(parameters[:method_name].to_s)
3
+ def tap_init!(klass, options = {}, &block)
4
+ TappingDevice.new(options, &block).tap_init!(klass)
87
5
  end
88
6
 
89
- def get_tapping_device(object)
90
- object.instance_variable_get(TAPPING_DEVICE)
7
+ def tap_assoc!(record, options = {}, &block)
8
+ TappingDevice.new(options, &block).tap_assoc!(record)
91
9
  end
92
10
 
93
- def add_tapping_device(object, trace_point)
94
- object.instance_variable_set(TAPPING_DEVICE, []) unless get_tapping_device(object)
95
- object.instance_variable_get(TAPPING_DEVICE) << trace_point
11
+ def tap_on!(object, options = {}, &block)
12
+ TappingDevice.new(options, &block).tap_on!(object)
96
13
  end
97
14
  end
98
15
  end
@@ -1,3 +1,3 @@
1
- module TappingDevice
2
- VERSION = "0.1.1"
1
+ class TappingDevice
2
+ VERSION = "0.2.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tapping_device
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - st0012
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-10-20 00:00:00.000000000 Z
11
+ date: 2019-11-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -115,6 +115,7 @@ files:
115
115
  - bin/console
116
116
  - bin/setup
117
117
  - lib/tapping_device.rb
118
+ - lib/tapping_device/exceptions.rb
118
119
  - lib/tapping_device/trackable.rb
119
120
  - lib/tapping_device/version.rb
120
121
  - tapping_device.gemspec