tapping_device 0.1.1 → 0.2.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 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