rails-dev-boost 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.
@@ -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