rails-dev-boost 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,171 @@
1
+ module RailsDevelopmentBoost
2
+ module DependenciesPatch
3
+ module InstrumentationPatch
4
+ module Instrumenter
5
+ delegate :boost_log, :to => 'ActiveSupport::Dependencies'
6
+
7
+ def self.included(mod)
8
+ mod.extend(ClassMethods)
9
+ end
10
+
11
+ module ClassMethods
12
+ def included(klass)
13
+ (public_instance_methods(false) + private_instance_methods(false) + protected_instance_methods(false)).each do |method|
14
+ if m = method.to_s.match(/\A(.+)_with_(.+)\Z/)
15
+ meth_name, extension = m[1], m[2]
16
+ extension.sub!(/[?!=]\Z/) do |modifier|
17
+ meth_name << modifier
18
+ ''
19
+ end
20
+ klass.alias_method_chain meth_name, extension
21
+ end
22
+ end
23
+
24
+ super
25
+ end
26
+ end
27
+ end
28
+
29
+ module LoadedFile
30
+ include Instrumenter
31
+
32
+ def boost_inspect
33
+ "\#<LoadedFile #{relative_path} #{inspect_constants(@constants)}>"
34
+ end
35
+
36
+ def add_constants_with_instrumentation(new_constants)
37
+ boost_log('ADD_CONSTANTS', "#{boost_inspect} <- #{inspect_constants(new_constants)}")
38
+ add_constants_without_instrumentation(new_constants)
39
+ end
40
+
41
+ def unload_dependent_file_with_instrumentation(dependent_file)
42
+ boost_log('UNLOAD_DEPENDENT', "#{boost_inspect}: #{dependent_file.boost_inspect}")
43
+ unload_dependent_file_without_instrumentation(dependent_file)
44
+ end
45
+
46
+ private
47
+ RAILS_ROOT = /\A#{Rails.root.to_s}/
48
+
49
+ def inspect_constants(constants_arr)
50
+ "[#{constants_arr.join(', ')}]"
51
+ end
52
+
53
+ def relative_path
54
+ @path.sub(RAILS_ROOT, '')
55
+ end
56
+
57
+ def self.included(klass)
58
+ klass.singleton_class.send :include, ClassMethods
59
+ super
60
+ end
61
+
62
+ module ClassMethods
63
+ include Instrumenter
64
+
65
+ def unload_modified_with_instrumentation!
66
+ boost_log('--- START ---')
67
+ unload_modified_without_instrumentation!.tap do
68
+ boost_log('--- END ---')
69
+ end
70
+ end
71
+
72
+ def unload_containing_file_with_instrumentation(const_name, file)
73
+ boost_log('UNLOAD_CONTAINING_FILE', "#{const_name} -> #{file.boost_inspect}")
74
+ unload_containing_file_without_instrumentation(const_name, file)
75
+ end
76
+ end
77
+ end
78
+
79
+ module Files
80
+ include Instrumenter
81
+
82
+ def unload_modified_file_with_instrumentation(file)
83
+ boost_log('CHANGED', "#{file.boost_inspect}")
84
+ unload_modified_file_without_instrumentation(file)
85
+ end
86
+
87
+ def unload_decorator_file_with_instrumentation(file)
88
+ boost_log('UNLOAD_DECORATOR_FILE', "#{file.boost_inspect}")
89
+ unload_decorator_file_without_instrumentation(file)
90
+ end
91
+ end
92
+
93
+ def self.apply!
94
+ unless applied?
95
+ ActiveSupport::Dependencies.extend self
96
+ RailsDevelopmentBoost::LoadedFile.send :include, LoadedFile
97
+ RailsDevelopmentBoost::LoadedFile::Files.send :include, Files
98
+ end
99
+ end
100
+
101
+ def self.applied?
102
+ ActiveSupport::Dependencies.singleton_class.include?(self)
103
+ end
104
+
105
+ def load_file_without_constant_tracking(path, *args)
106
+ other_args = ", #{args.map(&:inspect).join(', ')}" if args.any?
107
+ boost_log('LOAD', "load_file(#{path.inspect}#{other_args})")
108
+ super
109
+ end
110
+
111
+ def remove_constant_without_handling_of_connections(const_name)
112
+ boost_log('REMOVE_CONST', const_name)
113
+ super
114
+ end
115
+
116
+ def load_file_from_explicit_load(expanded_path)
117
+ boost_log('EXPLICIT_LOAD_REQUEST', expanded_path)
118
+ super
119
+ end
120
+
121
+ def boost_log(action, msg = nil)
122
+ action, msg = msg, action unless msg
123
+ raw_boost_log("#{ "[#{action}] " if action}#{msg}")
124
+ end
125
+
126
+ private
127
+ def unprotected_remove_constant(const_name)
128
+ boost_log('REMOVING', const_name)
129
+ @removal_nesting = (@removal_nesting || 0) + 1
130
+ super
131
+ ensure
132
+ @removal_nesting -= 1
133
+ end
134
+
135
+ def error_loading_file(file_path, e)
136
+ description = RailsDevelopmentBoost::LoadedFile.loaded?(file_path) ? RailsDevelopmentBoost::LoadedFile.for(file_path).boost_inspect : file_path
137
+ boost_log('ERROR_WHILE_LOADING', "#{description}: #{e.inspect}")
138
+ super
139
+ end
140
+
141
+ def remove_explicit_dependency(const_name, depending_const)
142
+ boost_log('EXPLICIT_DEPENDENCY', "#{const_name} -> #{depending_const}")
143
+ super
144
+ end
145
+
146
+ def remove_dependent_constant(original_module, dependent_module)
147
+ boost_log('DEPENDENT_MODULE', "#{original_module._mod_name} -> #{dependent_module._mod_name}")
148
+ super
149
+ end
150
+
151
+ def remove_autoloaded_parent_module(initial_object, parent_object)
152
+ boost_log('REMOVE_PARENT', "#{initial_object._mod_name} -> #{parent_object._mod_name}")
153
+ super
154
+ end
155
+
156
+ def remove_child_module_constant(parent_object, full_child_const_name)
157
+ boost_log('REMOVE_CHILD', "#{parent_object._mod_name} -> #{full_child_const_name}")
158
+ super
159
+ end
160
+
161
+ def remove_nested_constant(parent_const, child_const)
162
+ boost_log('REMOVE_NESTED', "#{parent_const} :: #{child_const.sub(/\A#{parent_const}::/, '')}")
163
+ super
164
+ end
165
+
166
+ def raw_boost_log(msg)
167
+ Rails.logger.info("[DEV-BOOST] #{"\t" * (@removal_nesting || 0)}#{msg}")
168
+ end
169
+ end
170
+ end
171
+ end
@@ -1,3 +1,5 @@
1
+ require 'active_support/descendants_tracker'
2
+
1
3
  module RailsDevelopmentBoost
2
4
  module DescendantsTrackerPatch
3
5
  def self.apply!
@@ -0,0 +1,18 @@
1
+ module RailsDevelopmentBoost
2
+ module LoadablePatch
3
+ def self.apply!
4
+ Object.send :include, LoadablePatch
5
+ end
6
+
7
+ def load(file, wrap = false)
8
+ expanded_path = File.expand_path(file)
9
+ # force the manual #load calls for autoloadable files to go through the AS::Dep stack
10
+ if ActiveSupport::Dependencies.in_autoload_path?(expanded_path)
11
+ expanded_path << '.rb' unless expanded_path =~ /\.(rb|rake)\Z/
12
+ ActiveSupport::Dependencies.load_file_from_explicit_load(expanded_path)
13
+ else
14
+ super
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,13 +1,98 @@
1
1
  module RailsDevelopmentBoost
2
2
  class LoadedFile
3
- @constants_to_files = {}
3
+ class Files < Hash
4
+ def initialize(*args)
5
+ super {|hash, file_path| hash[file_path] = LoadedFile.new(file_path)}
6
+ end
7
+
8
+ def unload_modified!
9
+ each_file_unload_if_changed {|file| file.changed?}
10
+ end
11
+
12
+ def each_file_unload_if_changed
13
+ unloaded_something = false
14
+ values.each do |file|
15
+ if yield(file)
16
+ unload_modified_file(file)
17
+ unloaded_something = true
18
+ end
19
+ end
20
+ if unloaded_something
21
+ values.each do |file|
22
+ unload_decorator_file(file) if file.decorator_like?
23
+ end
24
+ end
25
+ unloaded_something
26
+ end
27
+
28
+ def unload_modified_file(file)
29
+ file.unload!
30
+ end
31
+
32
+ def unload_decorator_file(file)
33
+ file.unload!
34
+ end
35
+
36
+ def constants
37
+ values.map(&:constants).flatten
38
+ end
39
+
40
+ def stored?(file)
41
+ key?(file.path) && self[file.path] == file
42
+ end
43
+
44
+ alias_method :loaded?, :key?
45
+ end
4
46
 
5
- class << self
6
- attr_reader :constants_to_files
47
+ class ConstantsToFiles < Hash
48
+ def associate(const_name, file)
49
+ (self[const_name] ||= []) << file
50
+ end
51
+
52
+ def deassociate(const_name, file)
53
+ if files = self[const_name]
54
+ files.delete(file)
55
+ delete(const_name) if files.empty?
56
+ end
57
+ end
58
+
59
+ def each_file_with_const(const_name, &block)
60
+ if files = self[const_name]
61
+ files.dup.each(&block)
62
+ end
63
+ end
64
+ end
65
+
66
+ class Interdependencies < Hash
67
+ def associate(file_a, file_b)
68
+ (self[file_a] ||= Set.new) << file_b
69
+ (self[file_b] ||= Set.new) << file_a
70
+ end
71
+
72
+ def each_dependent_on(file)
73
+ if deps = delete(file)
74
+ deps.each do |dep|
75
+ deassociate(dep, file)
76
+ yield dep
77
+ end
78
+ end
79
+ end
80
+
81
+ private
82
+ def deassociate(file_a, file_b)
83
+ if deps = self[file_a]
84
+ deps.delete(file_b)
85
+ delete(file_a) if deps.empty?
86
+ end
87
+ end
7
88
  end
8
89
 
90
+ LOADED = Files.new
91
+ CONSTANTS_TO_FILES = ConstantsToFiles.new
92
+ INTERDEPENDENCIES = Interdependencies.new
93
+ NOW_UNLOADING = Set.new
94
+
9
95
  attr_accessor :path, :constants
10
- delegate :constants_to_files, :to => 'self.class'
11
96
 
12
97
  def initialize(path, constants=[])
13
98
  @path = path
@@ -21,53 +106,145 @@ module RailsDevelopmentBoost
21
106
  end
22
107
 
23
108
  def add_constants(new_constants)
24
- new_constants.each do |new_constant|
25
- (constants_to_files[new_constant] ||= []) << self
26
- end
109
+ new_constants.each {|new_constant| CONSTANTS_TO_FILES.associate(new_constant, self)}
27
110
  @constants |= new_constants
28
- retrieve_associated_files.each {|file| file.add_constants(@constants)} if @associated_files
29
111
  end
30
112
 
31
- def delete_constant(const_name)
32
- delete_from_constants_to_files(const_name)
33
- @constants.delete(const_name)
113
+ # "decorator" files are popular with certain Rails frameworks (spree/refinerycms etc.) they don't define their own constants, instead
114
+ # they are usually used for adding methods to other classes via Model.class_eval { def meth; end }
115
+ def decorator_like?
116
+ @constants.empty? && !INTERDEPENDENCIES[self]
34
117
  end
35
118
 
36
- def associate_with(other_loaded_file)
37
- (@associated_files ||= []) << other_loaded_file
119
+ def unload!
120
+ guard_double_unloading do
121
+ INTERDEPENDENCIES.each_dependent_on(self) {|dependent_file| unload_dependent_file(dependent_file)}
122
+ @constants.dup.each {|const| ActiveSupport::Dependencies.remove_constant(const)}
123
+ clean_up_if_necessary
124
+ end
38
125
  end
39
126
 
40
- def retrieve_associated_files
41
- associated_files, @associated_files = @associated_files, nil
42
- associated_files
127
+ def associate_to_greppable_constants # brute-force approach
128
+ # we don't know anything about the constants contained in the files up the currently_loading stack
129
+ ActiveSupport::Dependencies.currently_loading.each {|path| self.class.relate_files(path, self)}
130
+ add_constants(greppable_constants)
43
131
  end
44
132
 
45
- def require_path
46
- @path.sub(/\.rb\Z/, '')
133
+ # It is important to catch all the intermediate constants as they might be "nested" constants, that are generally not tracked by AS::Dependencies.
134
+ # Pathological example is as follows:
135
+ #
136
+ # File `a.rb` contains `class A; X = :x; end`. AS::Dependencies only associates the 'A' const to the `a.rb` file (ignoring the nested 'A::X'), while
137
+ # a decorator file `b_decorator.rb` containing `B.class_eval {A::X}` would grep and find the `A::X` const, check that indeed it is in autoloaded
138
+ # namespace and associate it to itself. When `b_decorator.rb` is then being unloaded it simply does `remove_constant('A::X')` while failing to trigger
139
+ # the unloading of `a.rb`.
140
+ def greppable_constants
141
+ constants = []
142
+ read_greppable_constants.each do |const_name|
143
+ intermediates = nil
144
+ begin
145
+ if self.class.loaded_constant?(const_name)
146
+ constants << const_name
147
+ constants.concat(intermediates) if intermediates
148
+ break
149
+ end
150
+ (intermediates ||= []) << const_name.dup
151
+ end while const_name.sub!(/::[^:]+\Z/, '')
152
+ end
153
+ constants.uniq
154
+ end
155
+
156
+ def read_greppable_constants
157
+ File.read(@path).scan(/[A-Z][_A-Za-z0-9]*(?:::[A-Z][_A-Za-z0-9]*)*/).uniq
47
158
  end
48
159
 
49
- def self.each_file_with_const(const_name, &block)
50
- if files = constants_to_files[const_name]
51
- files.dup.each(&block)
160
+ # consistent hashing
161
+ def hash
162
+ @path.hash
163
+ end
164
+
165
+ def eql?(other)
166
+ @path.eql?(other)
167
+ end
168
+
169
+ def unload_dependent_file(dependent_file)
170
+ dependent_file.unload!
171
+ end
172
+
173
+ def guard_double_unloading
174
+ if NOW_UNLOADING.add?(self)
175
+ begin
176
+ yield
177
+ ensure
178
+ NOW_UNLOADING.delete(self)
179
+ end
52
180
  end
53
181
  end
54
182
 
55
- def stale!
56
- @mtime = 0
57
- if associated_files = retrieve_associated_files
58
- associated_files.each(&:stale!)
183
+ def delete_constant(const_name)
184
+ CONSTANTS_TO_FILES.deassociate(const_name, self)
185
+ @constants.delete(const_name)
186
+ clean_up_if_necessary
187
+ end
188
+
189
+ def clean_up_if_necessary
190
+ if @constants.empty? && LOADED.stored?(self)
191
+ LOADED.delete(@path)
192
+ ActiveSupport::Dependencies.loaded.delete(require_path)
59
193
  end
60
194
  end
61
195
 
62
- private
196
+ def associate_with(other_loaded_file)
197
+ INTERDEPENDENCIES.associate(self, other_loaded_file)
198
+ end
199
+
200
+ def require_path
201
+ File.expand_path(@path.sub(/\.rb\Z/, '')) # be sure to do the same thing as Dependencies#require_or_load and use the expanded path
202
+ end
63
203
 
64
- def delete_from_constants_to_files(const_name)
65
- if files = constants_to_files[const_name]
66
- files.delete(self)
67
- constants_to_files.delete(const_name) if files.empty?
204
+ def stale!
205
+ @mtime = 0
206
+ INTERDEPENDENCIES.each_dependent_on(self, &:stale!)
207
+ end
208
+
209
+ class << self
210
+ def unload_modified!
211
+ LOADED.unload_modified!
212
+ end
213
+
214
+ def for(file_path)
215
+ LOADED[file_path]
216
+ end
217
+
218
+ def loaded?(file_path)
219
+ LOADED.loaded?(file_path)
220
+ end
221
+
222
+ def loaded_constants
223
+ LOADED.constants
224
+ end
225
+
226
+ def loaded_constant?(const_name)
227
+ CONSTANTS_TO_FILES[const_name]
228
+ end
229
+
230
+ def unload_files_with_const!(const_name)
231
+ CONSTANTS_TO_FILES.each_file_with_const(const_name) {|file| unload_containing_file(const_name, file)}
232
+ end
233
+
234
+ def unload_containing_file(const_name, file)
235
+ file.unload!
236
+ end
237
+
238
+ def const_unloaded(const_name)
239
+ CONSTANTS_TO_FILES.each_file_with_const(const_name) {|file| file.delete_constant(const_name)}
240
+ end
241
+
242
+ def relate_files(base_file, related_file)
243
+ LOADED[base_file].associate_with(LOADED[related_file])
68
244
  end
69
245
  end
70
246
 
247
+ private
71
248
  def current_mtime
72
249
  # trying to be more efficient: there is no need for a full-fledged Time instance, just grab the timestamp
73
250
  File.mtime(@path).to_i rescue nil